Skip to content

fix(browse): block the whole IPv4 link-local range (169.254.0.0/16) in navigation URL validation - #2811

Open
gabrielrondon wants to merge 1 commit into
garrytan:mainfrom
gabrielrondon:fix/browse-ipv4-link-local-ssrf
Open

fix(browse): block the whole IPv4 link-local range (169.254.0.0/16) in navigation URL validation#2811
gabrielrondon wants to merge 1 commit into
garrytan:mainfrom
gabrielrondon:fix/browse-ipv4-link-local-ssrf

Conversation

@gabrielrondon

Copy link
Copy Markdown

What

validateNavigationUrl blocks the cloud metadata IP 169.254.169.254 by exact match but allows the rest of the IPv4 link-local range 169.254.0.0/16. This blocks the whole /16, matching the range treatment the same file already gives IPv6 link-local (fe80::/10) and ULA (fc00::/7), and the cookie-domain guard in session-persist.ts.

Why

The navigation gate is the control that stops a prompt-injected page from steering the daemon's Chrome to an internal address (threat note in browse/test/url-validation.test.ts and browse/src/write-commands.ts). Today it covers IPv4 unevenly:

  • IPv6: whole ranges blocked (BLOCKED_IPV6_PREFIXES in url-validation.ts).
  • IPv4: only the single IP 169.254.169.254 (BLOCKED_METADATA_HOSTS).
  • session-persist.ts already blocks the whole range for cookie domains: if (/^169\.254\./.test(d)) return true; // link-local incl. cloud metadata, with a test asserting "whole link-local block, not just metadata".

So the primary navigation control is weaker than a guard next to it, and out of line with the project's own policy. The gap leaves other link-local endpoints reachable, notably the container credential services that hand out IAM credentials:

  • 169.254.170.2 (AWS ECS / Fargate task credentials, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI)
  • 169.254.170.23 (EKS Pod Identity)

Impact is defense-in-depth: it matters when the browse daemon runs in a cloud or container environment that exposes a link-local credentials service (CI on ECS/Fargate, for example). The unconditional part is the inconsistency itself.

Fix

isBlockedIpv4LinkLocal() matches 169.254.0.0/16 in dotted-quad form and in the IPv4-mapped IPv6 form the URL parser normalizes to (::ffff:a9fe:xxxx, where a9fe == 169.254). It is wired into the three places the IPv6 range check already runs: the direct navigation gate, and both the A and AAAA branches of the DNS-rebinding check. Numeric IPv4 forms (hex, octal, decimal) are covered because the URL parser normalizes them to dotted-quad before the check.

Only dotted-quad literals match, so real hostnames like 169.254.example.com are left to DNS resolution rather than blocked as strings. RFC1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and loopback stay allowed, so the local-QA use case is unchanged.

Tests

browse/test/url-validation.test.ts: the ECS and EKS credential endpoints, a generic in-range address, hex and IPv4-mapped forms, plus two negatives (a 169.254.-prefixed hostname is not string-blocked, and private dev servers still pass). Full file: 59 pass, 0 fail.

…n navigation URL validation

validateNavigationUrl blocked the cloud metadata IP 169.254.169.254 by exact
match but allowed the rest of the IPv4 link-local range 169.254.0.0/16. The same
file already blocks IPv6 link-local (fe80::/10) and ULA (fc00::/7) as whole
ranges, and session-persist.ts blocks the whole IPv4 range for cookie domains
(/^169\.254\./, "whole link-local block, not just metadata"). The navigation
gate, the primary control against a prompt-injected page reaching an internal
address, was the weaker one.

The gap leaves the container credential endpoints reachable: 169.254.170.2 (AWS
ECS/Fargate task credentials) and 169.254.170.23 (EKS Pod Identity).

Block the whole /16 via isBlockedIpv4LinkLocal(), covering dotted-quad and the
IPv4-mapped IPv6 form the URL parser normalizes to (::ffff:a9fe:xxxx). Wired into
the navigation gate and both branches of the DNS-rebinding check. Numeric IPv4
forms are covered because the parser normalizes them first. RFC1918 private
ranges and loopback stay allowed, so local QA is unchanged.

Claude-Session: https://claude.ai/code/session_01KjBowH1wmRk6TZUwWGfo3b
@trunk-io

trunk-io Bot commented Sep 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant