Skip to content

Strip port from the raw remote address, not just X-Forwarded-For#306

Open
Mishenevd wants to merge 1 commit into
mainfrom
fix/strip-port-from-remote-address
Open

Strip port from the raw remote address, not just X-Forwarded-For#306
Mishenevd wants to merge 1 commit into
mainfrom
fix/strip-port-from-remote-address

Conversation

@Mishenevd

@Mishenevd Mishenevd commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

ProxyForwardedParser only strips an ephemeral source port when the client IP comes from X-Forwarded-For. When there is no trusted forwarding header (no proxy, or AIKIDO_TRUST_PROXY=false), getIpFromRequest() returns the raw socket address as-is:

// If no valid IP was found, or if X-Forwarded-For was not present, default to raw ip:
return rawIp;

So an ip:port value (e.g. 109.132.232.101:58780) can flow straight through into attacks, rate limiting and the reported user IPs.

On the cloud side this is the root cause of duplicate runtime-user IP rows — the dedup key there is user_id|ip_address, so the same real client IP with different source ports is stored as separate rows. It also previously crashed the GeoIP lookup (InvalidArgumentException: The value "109.132.232.101:58780" is not a valid IP address).

The X-Forwarded-For strip has shipped since the first release (v0.1.4), yet the cloud still received ported IPv4 values from Java apps — because they arrive via this raw-IP path, which was never normalized.

Fix

  • Extract a stripPort() helper and apply it to the raw-IP fallback as well, so the port is removed regardless of whether the IP came from a header or the socket.
  • stripPort() handles IPv4 with a port (1.2.3.4:5678) and bracketed IPv6 with a port ([2001:db8::1]:5678), and leaves already-valid IPs (including bare IPv6) and non-IP values untouched.
  • extractIpFromHeader() now reuses the same helper, which also adds bracketed-IPv6 port stripping to the X-Forwarded-For path (previously IPv4-only).

Tests

Added to ProxyForwardedParserTest:

  • port stripped from the raw IP fallback (with and without a forwarding header / AIKIDO_TRUST_PROXY=false),
  • bare IPv6 raw IP left unchanged,
  • bracketed IPv6 with a port stripped in X-Forwarded-For,
  • direct stripPort() unit coverage (IPv4:port, bare/bracketed IPv6, whitespace, non-IP passthrough, null).

All ProxyForwardedParserTest cases pass. The remaining failures in the full :agent_api:test run are pre-existing and environment-related (SSRF tests needing DNS, MySQL/MariaDB/MSSQL tests needing live databases, platform musl/gnu binary path) — none touch this code.

Related

Complements the cloud-side guard in aikido-core that normalizes the IP at ingestion (defense in depth). This PR removes the port at the source; the core guard catches any older/other-language agents that still send it.

Summary by Aikido

Security Issues: 0 Quality Issues: 0 Resolved Issues: 0

⚡ Enhancements

  • Introduced stripPort helper, updated header parsing, and added tests

🐛 Bugfixes

  • Stripped ephemeral ports from raw socket IP fallback to normalize addresses

More info

ProxyForwardedParser only stripped an ephemeral source port when the IP came
from X-Forwarded-For. When there is no trusted forwarding header it returns
the raw socket address as-is, so an "ip:port" value (e.g. "1.2.3.4:58780")
leaks through to attacks, rate limiting and the reported user IPs. On the
cloud side this produces duplicate runtime-user IP rows (one per source port).

Extract a stripPort() helper and apply it to the raw IP fallback as well.
It handles IPv4 with a port and bracketed IPv6 with a port, and leaves
already-valid IPs (including bare IPv6) and non-IP values untouched. The
X-Forwarded-For parsing now reuses the same helper, which also adds bracketed
IPv6 port stripping there.
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...do/agent_api/helpers/net/ProxyForwardedParser.java 83.33% 1 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant