A video downloader is, at its core, a URL fetcher. A stranger pastes a link into your site, your server goes and fetches something from that link, and hands the result back. If you squint, that is the exact shape of a server-side request forgery hole: I made a web page that asks my server to fetch arbitrary URLs on demand.
When I built the download proxy on ramrom.io, this kept me up at night. Get it wrong and an attacker does not just download videos — they make my server request internal addresses, probe services that should never be reachable from the internet, and read the responses back through my own proxy. So before I ever let a user-supplied URL reach cURL, I built guard rails. None of them are clever. Together they are the whole defence.
Guard rail 1: only http and https. The first check is embarrassingly simple: parse the URL and reject any scheme that is not http or https. No file://, no gopher://, no dict://. On plain PHP this is five lines with parse_url(), and it kills a whole family of attacks before they start. You would be surprised how many quick tutorials skip it.
$scheme = parse_url($url, PHP_URL_SCHEME); if (!in_array(strtolower($scheme), ['http', 'https'], true)) { http_response_code(400); exit('That link does not look like a web address.'); } Guard rail 2: resolve the host and block the private ranges. A URL can point anywhere, including at 169.254.169.254 (cloud metadata), 127.0.0.1, or a 10.x internal address. My proxy resolves the hostname itself with gethostbyname() and checks the result against the private ranges — 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 — plus a block on non-public suffixes. If the host resolves to something that is not a public internet address, the request dies with a 400 and an honest message. It does not retry, it does not log the visitor's IP in a way that matters, it just refuses.
Guard rail 3: only follow redirects to safe places. Here is the one people forget. You check the first URL, it passes, and then the response says "actually, go here instead" — and "here" is an internal address. So the redirect follower is not cURL's FOLLOWLOCATION; it is a hand-rolled loop, capped at five hops, and every single hop goes back through the same scheme and private-range checks. A URL that starts clean but redirects into the basement is stopped at the hop where it turns bad.
for ($hop = 0; $hop < 5; $hop++) { guard_url($url); // scheme + DNS + private-range checks $res = curl_head($url, 15); // 15s timeout, headers only $next = redirect_target($res); if (!$next) { break; } $url = $next; } Guard rail 4: verify what came back. The response is checked before it ever touches the visitor's browser. I read the Content-Type and the first bytes of the body to confirm it is actually a media file, not an HTML error page or something weirder wearing a .mp4 name. The proxy streams the file through in chunks rather than loading it into memory, because a "download" that is really a 2 GB disk-fill attack should not live on my shared hosting account for even a moment. There is also a hard cap on response size — anything past it is cut off.
Guard rail 5: fail loudly in the logs, quietly to the user. Every blocked request writes one line to the server error log: the reason, the host that was rejected, nothing about the visitor. The visitor gets a plain "that link cannot be downloaded" message. This keeps the log a readable record of probing attempts without turning the UI into a security lecture.
What I learned building this: SSRF is not one bug, it is a gap between where you check and where you fetch. The scheme check means nothing if redirects are not checked too. The DNS check means nothing if you let cURL resolve the hostname itself a second time (DNS rebinding is a real thing; resolving once and fetching the checked IP is the boring correct answer). Every shortcut in this chain is a door.
None of this required a WAF, a cloud proxy, or anything beyond what plain PHP ships with — parse_url(), gethostbyname(), and a disciplined for loop. If your project fetches user-supplied URLs on shared hosting, these five guard rails will take you an afternoon and close the most embarrassing hole your app has.