Skip to content

security: block cloud-metadata SSRF targets in snapshot navigation + discovery (PER-8614)#2333

Open
Shivanshu-07 wants to merge 2 commits into
masterfrom
security/PER-8614-block-metadata-ssrf
Open

security: block cloud-metadata SSRF targets in snapshot navigation + discovery (PER-8614)#2333
Shivanshu-07 wants to merge 2 commits into
masterfrom
security/PER-8614-block-metadata-ssrf

Conversation

@Shivanshu-07

Copy link
Copy Markdown
Contributor

Summary

Snapshot URLs and discovered subresource requests were navigated/fetched by Chromium without validating the target host. A malicious or compromised snapshot URL (or a subresource it references) could point Chromium at a cloud instance-metadata endpoint and exfiltrate short-lived cloud credentials — an SSRF (F-016 / PER-8614).

This PR adds a surgical block of cloud-metadata endpoints only. It deliberately does not block RFC1918 / loopback / link-local ranges in general, because snapshotting http://localhost:3000 and internal staging hosts is a core Percy use case.

What is blocked

isMetadataTarget(url) / assertNotMetadataTarget(url) (in packages/core/src/utils.js) return/throw when the URL host is a known cloud instance-metadata endpoint:

  • Literal IPs: 169.254.169.254 (AWS/Azure/GCP IMDS), 169.254.170.2 (ECS task metadata), 100.100.100.200 (Alibaba), and IPv6 fd00:ec2::254 (AWS IMDS v6).
  • Hostnames (case-insensitive, exact or trailing-dot): metadata.google.internal, metadata.goog.
  • DNS-rebinding defense: the hostname is also resolved (dns.promises.lookup, all addresses); the request is blocked if any resolved address is a metadata IP. Resolution failures are non-fatal — only a positive metadata match blocks — so offline/localhost snapshots keep working.

Where the check is applied

  1. Top-level navigationassertNotMetadataTarget(snapshot.url) before page.goto(snapshot.url, ...) in packages/core/src/discovery.js.
  2. Discovered subresource requestsisMetadataTarget(url) in sendResponseResource (packages/core/src/network.js), gating the outbound Fetch.continueRequest path so a page cannot pivot to IMDS via a subresource. On a match the request is failed (Aborted) with a clear warn log: Refusing to fetch resource from cloud metadata endpoint: <host>. Cache hits and root resources are served earlier and never reach this branch, so no behavior changes for them.

What is intentionally still allowed

  • Loopback127.0.0.1, localhost, [::1].
  • General RFC1918 private hosts — e.g. 192.168.x.x, 10.x.x.x (internal staging).
  • Normal public hosts.

No behavior is altered for any non-metadata host.

Tests

Added unit tests in packages/core/test/utils.test.js covering: metadata IPs (v4 + v6) blocked, metadata hostnames (case-insensitive, trailing dot) blocked, DNS-rebinding to a metadata IP blocked, and localhost / 127.0.0.1 / [::1] / 192.168.1.10 / 10.0.0.5 / a public host all still allowed, plus DNS-failure and unparseable-URL cases.

All 10 new specs pass (yarn workspace @percy/core test --node). node --check passes on all edited source files.

🤖 Generated with Claude Code

…discovery (PER-8614)

Snapshot URLs and discovered subresource requests were navigated/fetched by
Chromium without validating the target host, allowing SSRF to cloud
instance-metadata endpoints (AWS/Azure/GCP IMDS, ECS task metadata, Alibaba,
GCP metadata hostnames) to exfiltrate short-lived cloud credentials.

Add isMetadataTarget/assertNotMetadataTarget helpers that block only known
metadata endpoints (literal IPs incl. IPv6, metadata hostnames, and any
hostname that resolves to a metadata IP to defeat DNS rebinding). Applied
before top-level navigation (discovery.js) and before issuing outbound
subresource requests (network.js). Loopback and general RFC1918 hosts remain
allowed so localhost/internal-staging snapshotting is unaffected. DNS
resolution failures are non-fatal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07 Shivanshu-07 requested a review from a team as a code owner July 6, 2026 05:39
Add a discovery spec exercising the subresource cloud-metadata block path in
network.js (instrumentation + Fetch.failRequest) and a utils spec covering the
canonicalHost fallback when a resolved address is not a parseable IPv6 literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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