Find the local file read that bypasses the image check in PHP
This PHP endpoint fetches a URL, checks it looks like an image, then streams it. Besides SSRF, find the vulnerability and how to bypass the mime check.
Constraints:
urlis fully attacker-controlled- the only guard is a
getimagesize()mime check beforereadfile()
$url = $_GET['url'];
$info = getimagesize($url);
if (stripos($info['mime'], 'image') === false) die();
readfile($url);
Diagnose the cause.
Local File Read — readfile($url) serves any path, so a php://filter wrapper reads arbitrary files. The mime gate is bypassed because a filter chain can re-encode the leading bytes until getimagesize() accepts them as an image, or via TOCTOU between check and read. Fix: allowlist sources, block php://, download once, re-encode server-side.
- ✗Considering the getimagesize() check sufficient protection against file reads
- ✗Seeing only SSRF and missing the Local File Read via php://filter
- ✗Overlooking the TOCTOU between the check and readfile
- →How does a php://filter chain turn a file's bytes into something resembling an image?
- →Why does TOCTOU bypass the mime check even without the php:// wrapper?
The vulnerability
$url = $_GET['url'];
$info = getimagesize($url);
if (stripos($info['mime'], 'image') === false) die();
readfile($url);
readfile($url) serves the content of any path → Local File Read. The getimagesize() check is bypassed:
- php://filter chain. The
php://filterwrapper can apply a chain of encoding filters to a stream. Chosen carefully, they make the start of the stream look like bytesgetimagesize()counts as an image, whilereadfilestill returns the original contents of an arbitrary local file. The essence: one representation is validated, a different one is served. - TOCTOU.
getimagesize()sees a valid image,readfile()gets the real payload (if the URL returns different responses per request).
The fix
$allowed = ['https://cdn.example.com/'];
if (!startsWithAny($url, $allowed) || str_starts_with($url, 'php://')) die();
$data = downloadOnce($url); // one request — no TOCTOU
if (!isRealImage($data)) die();
echo reencodeImage($data); // re-encoding strips an embedded payload
✅ A source allowlist, blocking the php:// wrapper, a single download (no TOCTOU), and server-side re-encoding remove the LFR at its root.