PHPackages                             securized/laravel-ssrf - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. securized/laravel-ssrf

ActiveLibrary[Security](/categories/security)

securized/laravel-ssrf
======================

SSRF prevention for Laravel. Protect Http::, Guzzle, and validate user-supplied URLs against server-side request forgery.

1.0.0(yesterday)041↑2607.3%MITPHP ^8.4

Since Jul 22Compare

[ Source](https://github.com/securized/laravel-ssrf)[ Packagist](https://packagist.org/packages/securized/laravel-ssrf)[ Docs](https://github.com/securized/laravel-ssrf)[ RSS](/packages/securized-laravel-ssrf/feed)WikiDiscussions Synced today

READMEChangelog (1)Dependencies (14)Versions (2)Used By (0)

Laravel SSRF Prevention
=======================

[](#laravel-ssrf-prevention)

[![Latest Version on Packagist](https://camo.githubusercontent.com/9fd322449ae921ce6da571b0e27fdb5faf1403afc86a8afcd023ec352eb55e54/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7365637572697a65642f6c61726176656c2d737372662e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/securized/laravel-ssrf)[![Tests](https://camo.githubusercontent.com/3903b74d0b4bdec32d6e5a58e71b666a27f278474b16dc6f45e2c851bdbca51a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7365637572697a65642f6c61726176656c2d737372662f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/securized/laravel-ssrf/actions/workflows/tests.yml)[![Quality](https://camo.githubusercontent.com/67e31c0b5f5e6942ea43ebd4ff4bd6ef63d8914742e89836b2428a9835aefecb/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7365637572697a65642f6c61726176656c2d737372662f7175616c6974792e796d6c3f6272616e63683d6d61696e266c6162656c3d7175616c697479267374796c653d666c61742d737175617265)](https://github.com/securized/laravel-ssrf/actions/workflows/quality.yml)[![Total Downloads](https://camo.githubusercontent.com/e0b774e7abcd3d2eeeb1cc2a3c4d01261f50cb3405fa7553c339d9097750eef3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7365637572697a65642f6c61726176656c2d737372662e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/securized/laravel-ssrf)

SSRF (Server-Side Request Forgery) prevention for Laravel. Protect `Http::`, raw Guzzle, and user-supplied URLs from being weaponised to reach internal infrastructure, cloud metadata endpoints, or private networks.

The problem
-----------

[](#the-problem)

Fetching a user-supplied URL is a normal thing to do. Webhooks, link previews, avatar imports, "import from URL" buttons. The code usually looks like this:

```
$response = Http::get($request->input('webhook_url'));
```

Your server will happily fetch whatever it's pointed at, from a network position your users don't have. So an attacker submits:

```
http://169.254.169.254/latest/meta-data/iam/security-credentials/

```

On unpatched EC2 (IMDSv1), that returns your instance's IAM credentials. The same trick reaches `http://localhost:6379` (Redis), your internal admin panel, or a Kubernetes API on a private subnet. Anything your server can route to but the internet can't.

Blocking this properly is harder than it looks. `127.0.0.1` is also `http://0177.0.0.1/`, `http://2130706433/`, and `http://[::1]/`. A hostname that resolves to a public IP when you check it can resolve to `127.0.0.1` a second later when Guzzle connects. This package handles those cases and validates every URL before the request leaves your application.

Installation
------------

[](#installation)

```
composer require securized/laravel-ssrf
```

Publish the config file:

```
php artisan vendor:publish --tag="ssrf-config"
```

Quick Start
-----------

[](#quick-start)

```
use Illuminate\Support\Facades\Http;

// Wrap any Http:: call with ->ssrf() to enable protection
Http::ssrf()->get($userSuppliedUrl);
Http::ssrf()->post($webhookUrl, $payload);
```

That's it. Requests to private IPs, localhost, link-local addresses (including cloud metadata endpoints), and non-HTTP(S) schemes are blocked out of the box.

Features
--------

[](#features)

- **Laravel HTTP Client macro**: `Http::ssrf()` and `Http::withSsrfProtection()`
- **Raw Guzzle middleware**: drop into any `HandlerStack`
- **Validation rule**: `new SsrfSafeUrl()` for form/API input validation
- **Facade**: `Ssrf::validate($url)`, `Ssrf::isSafe($url)`, `Ssrf::safeUrl($url)`
- **IPv4 + IPv6**: blocks private ranges for both address families
- **DNS pinning**: prevents DNS rebinding attacks
- **Configurable**: whitelist/blacklist for IPs, ports, domains, and schemes
- **Immutable options**: safe in long-running processes (Octane, RoadRunner)

Usage
-----

[](#usage)

### Laravel HTTP Client

[](#laravel-http-client)

The `ssrf()` and `withSsrfProtection()` macros return a `PendingRequest` and chain normally with all other HTTP client methods:

```
use Illuminate\Support\Facades\Http;

// Protect a single request
$response = Http::ssrf()->get($userUrl);

// Chain with other options
$response = Http::ssrf()
    ->withHeaders(['Accept' => 'application/json'])
    ->timeout(10)
    ->get($userUrl);

// Both macros are identical
Http::withSsrfProtection()->post($webhookUrl, $data);
```

Blocked requests throw a `\GuzzleHttp\Exception\RequestException` (the same exception Guzzle throws for failed requests), so your existing error handling works without changes.

### Global Protection

[](#global-protection)

To protect every outgoing `Http::` request automatically, set `auto_protect` in your `.env`:

```
SSRF_AUTO_PROTECT=true
```

Or in `config/ssrf.php`:

```
'auto_protect' => true,
```

### Raw Guzzle

[](#raw-guzzle)

Use the static factory to add SSRF protection to any Guzzle client without the Laravel container:

```
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Securized\Ssrf\Http\Middleware\SsrfProtectionMiddleware;

$stack = HandlerStack::create();
$stack->push(SsrfProtectionMiddleware::make());

$client = new Client(['handler' => $stack]);
$response = $client->get($userUrl);
```

### Validation Rule

[](#validation-rule)

Validate user-supplied URLs in form requests or controllers:

```
use Securized\Ssrf\Rules\SsrfSafeUrl;

$request->validate([
    'webhook_url' => ['required', 'url', new SsrfSafeUrl()],
    'preview_url' => ['required', 'url', new SsrfSafeUrl()],
]);
```

The validation error message deliberately does not expose internal network details to end users.

### Facade

[](#facade)

```
use Securized\Ssrf\Facades\Ssrf;

// Returns array{url: string, host: string, ips: list, pinned: bool}
// Throws SsrfException on failure.
$result = Ssrf::validate($url);

// Returns the validated URL string.
// When pin_dns is enabled, the host is replaced with the resolved IP.
// Always use this value for the actual request, not the original $url.
// Throws SsrfException on failure.
$safeUrl = Ssrf::safeUrl($url);

// Returns true/false, never throws.
if (!Ssrf::isSafe($url)) {
    abort(422, 'URL is not permitted.');
}
```

Configuration
-------------

[](#configuration)

After publishing, edit `config/ssrf.php`:

```
return [

    // Apply SSRF protection to all Http:: requests globally.
    'auto_protect' => env('SSRF_AUTO_PROTECT', false),

    // Allow credentials (user:pass@host) in URLs. Disabled by default.
    'send_credentials' => false,

    // Replace hostname with resolved IP before sending the request.
    // Prevents DNS rebinding attacks. See "DNS Pinning" below.
    'pin_dns' => false,

    'whitelist' => [
        'ips'     => [],               // CIDR ranges or exact IPs
        'ports'   => [80, 443, 8080],  // Allowed ports (empty = allow all)
        'domains' => [],               // Regex patterns (empty = allow all)
        'schemes' => ['http', 'https'],
    ],

    'blacklist' => [
        'ips' => [
            // RFC 1918 private ranges
            '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16',
            // Loopback
            '127.0.0.0/8',
            // Link-local (cloud metadata: AWS, GCP, Azure)
            '169.254.0.0/16',
            // ... and more (see config/ssrf.php for the full list)
        ],
        'ports'   => [],
        'domains' => [],
        'schemes' => [],
    ],
];
```

### Whitelist semantics

[](#whitelist-semantics)

An **empty whitelist** for a type means "allow all" (subject to the blacklist). A **non-empty whitelist** means only the listed values are permitted. The blacklist always takes precedence.

### Domain patterns

[](#domain-patterns)

Domain entries are literal hostnames with `*` as a wildcard, matched case-insensitively:

```
'whitelist' => [
    'domains' => [
        'api.example.com',      // exact host
        '*.trusted.com',        // any subdomain (not the bare apex)
    ],
],
```

Dots are literal, so a pattern never matches more hosts than it appears to. `api.example.com` matches that host and nothing else. Regex syntax is not supported; characters like `.` and `(` are matched literally.

### IP ranges

[](#ip-ranges)

IP entries support CIDR notation for both IPv4 and IPv6:

```
'blacklist' => [
    'ips' => [
        '10.0.0.0/8',   // IPv4 range
        'fc00::/7',     // IPv6 unique local
    ],
],
```

Per-Request Options
-------------------

[](#per-request-options)

Override the global config for a single request using `SsrfOptions`:

```
use Securized\Ssrf\SsrfOptions;

// Build from config and customise
$options = SsrfOptions::fromConfig(config('ssrf'))
    ->withPinDns()
    ->withWhitelistSchemes(['https'])  // HTTPS only for this request
    ->addBlacklistIp('203.0.113.0/24');

Http::ssrf($options)->get($url);

// Also works with the validation rule
new SsrfSafeUrl($options)
```

`SsrfOptions` is immutable. Each method returns a new instance, so customising per-request never affects shared state.

DNS Pinning
-----------

[](#dns-pinning)

DNS rebinding is an attack where a hostname initially resolves to a public IP (passing validation) but then re-resolves to a private IP for the actual request. Enabling `pin_dns` prevents this by resolving the hostname once, validating the IP, and then replacing the hostname in the URL with that IP for the actual request. The original `Host` header is preserved.

```
// In config/ssrf.php
'pin_dns' => true,

// Or per-request
Http::ssrf(SsrfOptions::fromConfig(config('ssrf'))->withPinDns())->get($url);
```

Note: DNS pinning may affect SSL certificate validation in some configurations.

What this does not protect against
----------------------------------

[](#what-this-does-not-protect-against)

Worth knowing before you rely on it:

- **Second-order requests are your responsibility.** Redirects *are* covered, as Guzzle re-enters the middleware for each hop, so a `302` to `http://169.254.169.254/` is blocked (there are tests for this). But if you fetch a page, parse a URL out of the body, and request it yourself, that second request needs its own validation.
- **TOCTOU without DNS pinning.** With `pin_dns` disabled, the hostname is resolved once for validation and again by Guzzle when it connects. An attacker controlling the DNS response can return a public IP for the first lookup and a private one for the second. Enable `pin_dns` to close this.
- **Blocked hosts are still distinguishable.** Timing and error differences let an attacker infer which internal hosts exist. This is a port scan with a very narrow oracle, not data exfiltration, but it is not nothing.
- **The blacklist covers documented ranges, not your network.** If your internal services live on public IPs, add them to the blacklist yourself, nothing here can infer that.

Exception Hierarchy
-------------------

[](#exception-hierarchy)

All SSRF exceptions extend `\Securized\Ssrf\Exceptions\SsrfException` (itself a `RuntimeException`):

```
SsrfException
└── InvalidUrlException      - URL cannot be parsed, is empty, or contains credentials
    ├── InvalidSchemeException  - scheme not permitted (e.g. file://, gopher://)
    ├── InvalidPortException    - port not permitted
    ├── InvalidDomainException  - hostname not permitted or does not resolve
    └── InvalidIpException      - resolved IP matches a blacklisted range

```

Catch `SsrfException` to handle any SSRF failure, or catch specific subclasses for fine-grained handling:

```
use Securized\Ssrf\Exceptions\InvalidIpException;
use Securized\Ssrf\Exceptions\SsrfException;
use Securized\Ssrf\Facades\Ssrf;

try {
    $safeUrl = Ssrf::safeUrl($userUrl);
} catch (InvalidIpException $e) {
    // Resolved to a private IP
} catch (SsrfException $e) {
    // Any other validation failure
}
```

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Credits
-------

[](#credits)

The threat model and much of the test suite structure come from [SSRF vs. Developers: A Study of SSRF-Defenses in PHP Applications](https://www.usenix.org/conference/usenixsecurity24/presentation/wessels) (USENIX Security '24) by Malte Wessels, Simon Koch, Giancarlo Pellegrino and Martin Johns, of TU Braunschweig and CISPA Helmholtz Center for Information Security.

Their Table 2 enumerates the three ways a URL validation layer gets evaded, and this package defends against each:

EvasionFixHereURL parser confusionWell-established, hardened parserGuzzle's PSR-7 `Uri`, plus parser differential testsDNS rebindingIP pinning`pin_dns`RedirectsRecheck on each redirectMiddleware runs per hopThe paper describes IP pinning as "the only reliable defense against attacks targeting local resources without unduly restricting the versatility of the SSR feature".

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

If you find a security issue in this package, please email `root@securized.dev` rather than opening a public issue.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance100

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/93bbe626477c4cc187200def96abc399e818337d204e1a0c723cbb4ab42a5959?d=identicon)[securized](/maintainers/securized)

---

Tags

laravelsecurityssrfsecurizedserver-side-request-forgery

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/securized-laravel-ssrf/health.svg)

```
[![Health](https://phpackages.com/badges/securized-laravel-ssrf/health.svg)](https://phpackages.com/packages/securized-laravel-ssrf)
```

###  Alternatives

[spatie/laravel-permission

Permission handling for Laravel 12 and up

12.9k102.4M1.5k](/packages/spatie-laravel-permission)[spatie/laravel-health

Monitor the health of a Laravel application

87512.0M177](/packages/spatie-laravel-health)[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
