File Protocol SSRF to LFI: Turning Fetchers Into Readers
SSRF and LFI get filed as separate bugs. Sometimes they are the same sink wearing two hats. If the server fetches a URL you control and the client understands file://, you are not scanning the network—you are reading the filesystem.
The chain is simple. The proof should not be.
Find a fetcher that returns the body
Webhook testers, link unfurlers, PDF renderers, image proxies, and "import from URL" features are the usual suspects. Prefer sinks that echo content or status into the HTTP response. Blind DNS-only SSRF is a different report.
Probe with a URL you own first. Confirm method, redirects, timeouts, and whether response bodies are truncated. Then flip the scheme:
file:///etc/passwdfile://localhost/etc/passwdfile:///c:/windows/win.inion Windows stacks- wrapper tricks such as
php://filter/convert.base64-encode/resource=index.phpwhen the app is PHP and passes your string into a stream-aware API
Stop at a low-sensitivity file. /etc/passwd or a public webroot file is enough for most programs. Do not exfiltrate secrets, private keys, or customer uploads "to show severity."
Wrappers and normalization fight you
Some runtimes resolve file: only for certain APIs. URLConnection may allow it while a locked-down HTTP client does not. Java and .NET apps sometimes block file: yet still honor custom protocols registered elsewhere.
Path normalization creates false negatives. Try encoded slashes, extra ../, symlink-friendly paths under /var/www, and file:///proc/self/environ only if scope and policy clearly allow process introspection. If the response is a generic 500, differentiate "scheme blocked" from "file missing" by comparing with file:///definitely-not-a-file-abc123.
PDF and headless Chrome sinks deserve special care. Chromium may allow file: in older configurations or via redirect chains. A redirect from your HTTPS canary to file:///etc/passwd can bypass a naive allowlist that only checked the first hop.
When the body never comes back
Not every file: success is reflected. The app might hash the bytes, store them as an attachment, or feed them to an antivirus scanner. Check object storage URLs issued after import, error messages that echo partial content, and timing differences between a tiny file and a missing path.
On PHP, expect:// and phar:// are related cousins. Mention them only when the sink is clearly stream-wrapper based; otherwise you dilute a clean file: report. One solid read of a harmless file beats a laundry list of untested wrappers.
Container images change the prize. Reading /etc/passwd in a minimal distroless image is less exciting than reading an application .env mounted into the pod. If you stumble into env files, redact aggressively in the report and rotate through the program's disclosed channel if keys were truly exposed during testing.
Report it as a read primitive
Frame the issue as: user-controlled URL reaches a server-side fetch; non-HTTP scheme permitted; file contents returned to the attacker. Include the exact parameter, one harmless file, and whether the read is partial, base64-wrapped, or rendered into a PDF.
Remediation is boring and correct: allowlist https hosts, disable file: and other schemes in the HTTP client, and never pass user URLs into language-level stream wrappers. If local templates must be loaded, use an internal path API with a fixed root—not a URL parser.
Ask yourself once before filing: would a reviewer reproduce this in ten minutes with only your steps? If the answer depends on a private lab layout, rewrite the proof against the target's own public webroot file.