Pular para o conteúdo
← Voltar para projetos

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

#Laravel SSRF Guard

Buy Me A Coffee

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

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 A and IPv6 AAAA records);
  • 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 GET that follows redirects but re-validates every hop, so a public host cannot 302 you 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 the Content-Type and 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.

Nova versão disponível.