HexBrain

Basecamp · Report #3445890

Link unfurling calls out to arbitrary URLs and the private-network guard misses link-local addresses

Validstatus
mediumseverity
62upvotes
22 Dec 2025disclosed

How it was found

Summary

  • /unfurl_link lets any signed-in user supply a URL that the server fetches for OpenGraph data.
  • The “private network” guard only blocks RFC1918/loopback/0.0.0.0 and ignores link-local ranges, so 169.254.0.0/16 (IMDS) and fe80::/10 are treated as public.
  • The unfurl fetcher then issues a server-side HTTP request to that link-local host, enabling SSRF against cloud metadata or other link-local services.

Affected Components

  • app/controllers/unfurl_links_controller.rb:3-8 (accepts untrusted url and triggers metadata fetch.)
  • app/models/opengraph/location.rb:25-39 (validates target via PrivateNetworkGuard.resolve and accepts link-local IPs.)
  • lib/restricted_http/private_network_guard.rb:9-20 (private_ip? omits ipaddr.link_local?, so 169.254.x.x and fe80::/10 are not blocked.)
  • app/models/opengraph/fetch.rb:12-74 (performs the Net::HTTP request to the resolved IP using server network context.)

Attack preconditions

  • Any authenticated user (CSRF token or API client) can reach /unfurl_link.

Steps to reproduce

  1. Sign in as a normal user and obtain the session/CSRF tokens.

  2. Send a request such as:

curl -X POST https://<host>/unfurl_link -H "Cookie: session_token=..." -H "X-CSRF-Token: <token>" -d 'url=http://169.254.169.254/latest/meta-data/'
  1. Because PrivateNetworkGuard.private_ip? returns false for link-local addresses (IPAddr.new("169.254.169.254").private? #=> false while link_local? #=> true), the guard permits the target and Opengraph::Fetch issues a GET from the server to 169.254.169.254. On cloud hosts his reaches IMDS; responses with text/html would be returned, otherwise the request is still made (blind SSRF), allowing metadata probing/timing and any link-local HTML endpoints to be exfiltrated.

Evidence/References

  • Guard logic lacks a link-local check, so resolve does not raise for 169.254.169.254 (lib/restricted_http/private_network_guard.rb:9-20).
  • Opengraph::Location#validate_url_is_public trusts that result and proceeds to fetch (app/models/opengraph/location.rb:25-38).
  • The fetcher performs the server-side request (app/models/opengraph/fetch.rb:12-74), driven directly by user input (app/controllers/unfurl_links_controller.rb:3-8).

Impact

  • Authenticated users can coerce the server to reach link-local resources, including cloud metadata services (IMDS) that should be unreachable, enabling credential theft or internal service probing/timing if an HTML response is available.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original