Validation and connection resolve separately
If the browser performs a fresh DNS lookup after validation, an attacker can change the answer. Connect only to an address from the validated set and preserve the original Host/SNI semantics.
A screenshot service accepts a URL and runs a network-capable browser near credentials and internal services. Validating only the first string turns the feature into an SSRF primitive after redirects, DNS changes or subresource loads.
A public hostname reaches loopback, cloud metadata, RFC1918, Kubernetes service DNS or a custom storage/webhook endpoint that the caller could not access directly.
Resolve every address, reject the whole answer set when any address is forbidden, pin navigation to the validated destination and repeat the policy at every egress boundary.
import { lookup } from "node:dns/promises";
import { isIP } from "node:net";
async function validateTarget(raw: string) {
const url = new URL(raw);
if (!new Set(["http:", "https:"]).has(url.protocol)) throw new Error("scheme blocked");
const records = isIP(url.hostname)
? [{ address: url.hostname }]
: await lookup(url.hostname, { all: true, verbatim: true });
if (records.length === 0 || records.some(({ address }) => isPrivateOrSpecial(address))) {
throw new Error("destination blocked");
}
return { url, addresses: records.map(({ address }) => address) };
}
// Production still needs canonical IP parsing, redirect revalidation,
// DNS pinning and request interception. A regex is not an IP policy.If the browser performs a fresh DNS lookup after validation, an attacker can change the answer. Connect only to an address from the validated set and preserve the original Host/SNI semantics.
Every hop is a new target. A public 302 endpoint is enough to reach metadata unless the navigation is paused and the destination is validated again.
Images, fonts, iframes, scripts, fetch and websocket requests are just as capable of reaching internal services as the main document.
Webhook URLs and S3-compatible endpoints are user-controlled egress. Applying a weaker helper there recreates the same vulnerability outside Chromium.
A capture only after URL, scheme, port, hostname and every resolved address pass one shared policy; redirects and browser subrequests are revalidated before leaving the process.
One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.
The destination resolves to a blocked address, uses a blocked port or fails during connection.
HTTP 500 · retryable
DNS returns no usable address for the target.
HTTP 400 · terminal
The runner above carries this exact configuration into the playground — no retyping, no starting over.
Written by PageCapture Engineering. Reviewed by PageCapture security maintainers. Verified 2026-08-12 by running the published code against a fixture.
The security corpus covers private and special IPv4/IPv6 forms, encoded literals, blocked hostnames, ports and redirects; the public recipe is normalized separately and never exposes a private-target oracle.
Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.
Threat model and layered SSRF mitigations.
Defines the familiar private IPv4 ranges; the implementation blocks additional special-use ranges too.
Documents the exact shared egress policy and secret lifecycle.