Laravel Ssrf Guard
A Laravel package that protects outbound HTTP requests from SSRF (Server-Side Request Forgery): it validates that a URL's host resolves only to public IPs (denying private, reserved, loopback and link-local ranges by default), pins the connection to the validated IP to close the DNS-rebinding TOCTOU window, and performs safe GET requests that re-validate every redirect hop.
#Laravel SSRF Guard
Whenever your application fetches a URL that came from a user — an avatar URL, a webhook target, a link preview, an imported og:image — it can be tricked into reaching internal services instead: http://localhost, http://10.0.0.1, or the cloud metadata endpoint http://169.254.169.254. That is Server-Side Request Forgery (SSRF).
Laravel SSRF Guard makes fetching untrusted URLs safe. It:
- validates that a URL's host resolves only to public IP addresses, denying private, reserved, loopback and link-local ranges by default (both IPv4
Aand IPv6AAAArecords); - pins the connection to the validated IP via curl's
CURLOPT_RESOLVE, closing the DNS-rebinding (TOCTOU) window where a domain flips to an internal IP between validation and connection; - performs a safe
GETthat follows redirects but re-validates every hop, so a public host cannot302you into an internal one.
#Installation
You can install the package via composer:
composer require jeffersongoncalves/laravel-ssrf-guard
You can publish the config file with:
php artisan vendor:publish --tag="ssrf-guard-config"
This is the published config file:
return [ 'timeout' => (int) env('SSRF_GUARD_TIMEOUT', 8), 'max_redirects' => (int) env('SSRF_GUARD_MAX_REDIRECTS', 5), 'allowed_schemes' => ['http', 'https'], 'allow_private' => (bool) env('SSRF_GUARD_ALLOW_PRIVATE', false), 'max_response_size' => (int) env('SSRF_GUARD_MAX_RESPONSE_SIZE', 10 * 1024 * 1024), ];
#Usage
Resolve the guard from the container (it is registered as a singleton) or instantiate it directly.
#Check whether a URL is safe to fetch
use JeffersonGoncalves\SsrfGuard\SsrfGuard; $guard = app(SsrfGuard::class); $guard->isPublicUrl('https://example.com'); // true $guard->isPublicUrl('http://127.0.0.1'); // false (loopback) $guard->isPublicUrl('http://10.0.0.1'); // false (private) $guard->isPublicUrl('http://169.254.169.254'); // false (link-local / metadata) $guard->isPublicUrl('ftp://example.com'); // false (scheme not allowed)
Use it to validate user input before storing or fetching it:
if (! app(SsrfGuard::class)->isPublicUrl($request->input('webhook_url'))) { abort(422, 'The URL must point to a public host.'); }
#Fetch a URL safely
safeGet() performs the pinned, redirect-re-validating request and returns a standard Illuminate\Http\Client\Response. It throws JeffersonGoncalves\SsrfGuard\Exceptions\BlockedHostException if the URL — or any redirect hop — is not public:
use JeffersonGoncalves\SsrfGuard\SsrfGuard; use JeffersonGoncalves\SsrfGuard\Exceptions\BlockedHostException; try { $response = app(SsrfGuard::class)->safeGet($untrustedUrl); $body = $response->body(); } catch (BlockedHostException $e) { report($e); // The URL pointed at a non-public host — refuse to proxy it. }
You can pass extra HTTP Client options; they are merged over the safe defaults:
$response = app(SsrfGuard::class)->safeGet($url, [ 'headers' => ['Accept' => 'image/*'], ]);
safeGet()/safeRequest() also cap the response body at max_response_size (10 MiB by default), throwing JeffersonGoncalves\SsrfGuard\Exceptions\ResponseTooLargeException when an internal resource is larger than the cap — both the advertised Content-Length and the actual downloaded length are checked. Set the cap to 0 to disable it.
[!IMPORTANT]
safeGet()guarantees the transport is not pointed at an internal host. It does not validate the response body. If you re-serve fetched content from your own origin (images, HTML), still validate theContent-Typeand sanitize/transform the body to avoid stored XSS.
#Getting the curl resolve pin
If you build your own request and just want the validated CURLOPT_RESOLVE entries, call resolveEntries() — it returns null for any non-public/unsupported URL:
$resolve = app(SsrfGuard::class)->resolveEntries('https://example.com'); // ['example.com:443:93.184.216.34'] (or null if not public)
#Configuration
| Key | Default | Description |
|---|---|---|
timeout |
8 |
Maximum seconds a safeGet() request may run. |
max_redirects |
5 |
How many redirect hops to follow — each one is re-validated. |
allowed_schemes |
['http', 'https'] |
Schemes considered valid; everything else is rejected. |
allow_private |
false |
DANGER — when true, skips the private/reserved/loopback/link-local check. For local development only; never enable in production. |
max_response_size |
10485760 (10 MiB) |
Maximum response body size in bytes; larger responses throw ResponseTooLargeException. Set to 0 to disable. |
#Testing
composer test
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Contributing
Please see CONTRIBUTING for details.
#Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
#Credits
#License
The MIT License (MIT). Please see License File for more information.