Open source
Safe Fetch for Laravel
composer require roundly-consulting/safe-fetch-for-laravelOverview
SSRF protection for Laravel’s HTTP client. Make server-side requests to URLs you do not control — webhook targets, link previews, avatar, feed and import URLs — without reaching your own network. Every URL is parsed strictly and rebuilt, every address its host resolves to must be public, and the connection is pinned to exactly those addresses, redirects included. MIT-licensed, no database, built only on Laravel’s HTTP client and Roundly’s enums and package-toolkit.
What you get
One call on any request
Http::safe() guards the Laravel HTTP calls you already write — tokens, retries, pools and other middleware keep working. SafeFetch::guard() and http() do the same without the macro.
Internal destinations refused
Private, loopback, link-local, carrier-grade NAT and cloud-metadata addresses — AWS, Google Cloud, Azure, Alibaba, Oracle, OpenStack — in every spelling: numeric forms, IPv4 inside IPv6, names that point inside.
DNS pinned, connection checked
The name is resolved once, every address judged, curl pinned to exactly those with no proxy and no reused connection, and the connected address checked before a byte is sent and again afterwards.
Redirects under control
Off by default. When on, every hop is vetted and pinned again, with a hop limit, no https to http, credentials kept to the first origin and only the headers you name forwarded.
Capped fetch and download
fetch() reads a GET into memory, download() streams it to a file swapped in only on success — both with a byte cap counted after decoding, one time budget and errors that say whether a retry can help.
Validation, events and a health check
The SafeUrl rule and safe_url with one neutral message, a DestinationRefused event for every refusal, and safe-fetch:check for a URL’s verdict or the environment, as text or JSON.
Facade, DI or actions
One SafeFetch facade, an injectable manager or single-purpose actions — plus a recording fake that keeps the real guard and a DNS table for tests.
Documentation
Use cases
Where the package fits — webhook senders, link previews, imports and media from URLs, customer-configured integrations and SSRF alerting.
Installation
Install via Composer — no migrations, safe defaults — then optionally publish the config and translations and check the environment.
Configuration
Every key of config/safe-fetch.php — http, ports, host and range lists, the app-host block, redirects, the fetch cap and budget, the resolver.
The SafeFetch facade
Tour the SafeFetch facade — vet, allows, precheck, guard, http, fetch, download, truncated, policy, networks, urls — and the Http::safe() macro.
DI and actions
Skip the facade: inject SafeFetchManager or call a single-purpose action — every action with its execute() signature and facade method.
Guarded requests
Http::safe() step by step — the pinned curl handler, per-hop vetting, pre-send and post-checks, refused options, pools, retries and the response cap.
Fetch and download
SafeFetch::fetch() reads a capped, guarded GET into memory; download() streams it to a file swapped in only on success. Failures and retries.
Validation and precheck
The SafeUrl rule and the safe_url string rule with one neutral message, SafeUrl::withoutLookup() and SafeFetch::precheck() for save time.
Destination policy
DestinationPolicy — the withers that tighten or loosen per call, host patterns, the order of the rules, private hosts and a policy built from scratch.
Redirects
Redirects are off by default; when on, every hop is vetted and pinned, with a hop limit, no downgrade, credentials and cookies kept per origin.
URLs and blocked networks
How URLs are canonicalised, the 26 blocked ranges, IPv4 inside IPv6, cloud-metadata endpoints, the inspection helpers and a resolver of your own.
Verdicts and exceptions
The Reason enum, the exception hierarchy, retryable() and blockedBy(), neutral messages that never name the URL, and the named constructors.
Events
DestinationRefused fires on every refusal with the reason, host, rule and where it was judged — never the URL — ready for SSRF alerting.
Artisan commands
safe-fetch:check judges a URL or checks the environment, as text or JSON; php artisan about shows the active policy as counts only.
Use in your own package
Recipes for packages that fetch their users’ URLs — a policy that drops the host’s carve-outs, the manager, throttling, retries and save-time prechecks.
Threat model
What the guard defends against — rebinding, disguised addresses, redirects, proxies, parser tricks, leaky errors — and what it does not guarantee.
Testing
SafeFetch::fake() records every verdict and keeps the real guard; fakeDns() answers lookups from a table; every assertion and the pin in a test.
Requirements
PHP 8.4+, Laravel 13, ext-curl to send guarded requests, ext-intl for international names, two Roundly packages — no database.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.