RAMYA LAKHANI

All log entries

What I was doing. Building /scan for this site — paste a public GitHub repository URL, the server reads a capped sample of files and a model looks over them. The whole value is that a stranger supplies the URL.

What I noticed. That is the textbook shape of server-side request forgery. An untrusted string becomes a request made from my infrastructure, with my network position and my credentials. The feature and the vulnerability are the same mechanism; the only thing separating them is what the server agrees to fetch.

So the constraint could not be validation-shaped. Blocklists lose here — you can spend a long time enumerating localhost, 127.0.0.1, [::1], 0.0.0.0, decimal-encoded IPs, 169.254.169.254, and a DNS name that resolves to any of them after you have checked it.

What the endpoint does instead:

const REPO_HOSTS = new Set(['github.com', 'www.github.com'])

The user-supplied string is only ever parsed to extract an owner and repository name. Every actual request goes to a constant origin — GitHub’s API — with those two segments interpolated. The URL I was handed is never the URL I fetch. Add redirect: 'manual' so a 302 cannot walk the request somewhere else, an AbortController so a slow host cannot hold a worker open, and byte caps so a large response cannot exhaust memory.

Why it matters for shipped code. Any “paste a link and we’ll fetch it” feature is this bug wearing a product name: preview cards, webhook testers, avatar-by-URL, import-from-URL, PDF renderers that resolve remote images. The question to ask in review is not “is the input validated” but “can the attacker influence the destination at all?” If the answer is yes, no amount of parsing makes it safe — the destination has to come from a constant on the server.

Open question. The allowlist is two hostnames, which is easy to be confident about and useless for anything else. I do not yet have a good general answer for a service that legitimately needs to fetch arbitrary user-supplied hosts, other than pushing the fetch into an isolated egress proxy with no credentials and no route to internal ranges.