NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

config/safe-fetch.php is the default policy behind Http::safe(), SafeFetch::vet(), SafeFetch::fetch() and the safe_url rule. The blocked-network table always applies; these settings only add to it or carve out of it. Publish it with --tag=safe-fetch-config:

use RoundlyConsulting\SafeFetch\Support\SystemHostResolver;

return [

    // http too (local development, internal tools). https only by default.
    'allow_http' => env('SAFE_FETCH_ALLOW_HTTP', false),

    // null => any port 1–65535; otherwise the only ports a URL may use, e.g. [443].
    'ports' => null,

    'hosts' => [
        // Non-empty => ONLY these hosts (exact or *.example.com). Still resolved,
        // judged and pinned: an allowed host never makes a private address OK.
        'allowed' => [],

        // Refused by name, before any lookup.
        'blocked' => [],

        // Names whose addresses may be private (e.g. minio.internal). Still
        // resolved and pinned.
        'private' => [],
    ],

    'ranges' => [
        // Addresses / CIDRs exempt from the blocked-network table, e.g.
        // ['127.0.0.1/32', '10.20.0.0/16']. 0.0.0.0/0 and ::/0 are refused.
        'allowed' => [],

        // Extra addresses / CIDRs always refused; they beat every allow.
        'blocked' => [],
    ],

    // The app's own host names (app.url's host + extra) are refused by name, and
    // app.url's address when it is written as one: a request to yourself through
    // a public load balancer is SSRF too.
    'app_hosts' => [
        'block' => true,
        'extra' => [],
    ],

    'redirects' => [
        // Off by default: a redirect is returned to the caller unfollowed.
        'enabled' => env('SAFE_FETCH_REDIRECTS', false),

        // Hops followed at most, 0–10.
        'max' => env('SAFE_FETCH_MAX_REDIRECTS', 3),

        // null => 307 and 308 (method and body kept); SafeFetch::fetch(), which
        // only sends GETs, follows 301, 302, 303, 307 and 308 then. Otherwise a
        // subset of those five.
        'statuses' => null,

        // https -> http.
        'allow_downgrade' => false,

        // Header names also sent to another origin a redirect leads to
        // (Accept, Content-Type, User-Agent and the like always are).
        'forward_headers' => [],
    ],

    'fetch' => [
        // SafeFetch::fetch() body cap in bytes, at least 1.
        'max_bytes' => env('SAFE_FETCH_MAX_BYTES', 5_242_880),

        // SafeFetch::fetch() time budget in seconds, 1–600, every hop included.
        'timeout' => env('SAFE_FETCH_TIMEOUT', 30),
    ],

    // The resolver behind every lookup: any class implementing
    // RoundlyConsulting\SafeFetch\Contracts\HostResolver.
    'resolver' => SystemHostResolver::class,

];

Every key

KeyEnvDefaultPurpose
allow_httpSAFE_FETCH_ALLOW_HTTPfalsehttp as well as https.
ports—nullnull for any port, or a non-empty list of 1–65535 — the only ports a URL may use.
hosts.allowed—[]Host patterns; non-empty means only these hosts. Still resolved, judged and pinned.
hosts.blocked—[]Host patterns refused by name, before any lookup.
hosts.private—[]Host patterns whose addresses may be private (minio.internal) — never a cloud-metadata address.
ranges.allowed—[]Addresses or CIDRs exempt from the blocked-network table; never 0.0.0.0/0, ::/0 or an IPv6 prefix carrying all of IPv4.
ranges.blocked—[]Addresses or CIDRs always refused; they beat every allow.
app_hosts.block—trueRefuse the app’s own host names, and app.url’s address when it is written as one.
app_hosts.extra—[]Host patterns that also mean the app — the host of app.url always does.
redirects.enabledSAFE_FETCH_REDIRECTSfalseFollow redirects; off returns a redirect to the caller unfollowed.
redirects.maxSAFE_FETCH_MAX_REDIRECTS3Hops followed at most, 0–10.
redirects.statuses—nullnull: 307 and 308 (all five for fetch() and download()); otherwise a subset of 301, 302, 303, 307, 308.
redirects.allow_downgrade—falseAllow https → http.
redirects.forward_headers—[]Header names also sent to another origin a redirect leads to.
fetch.max_bytesSAFE_FETCH_MAX_BYTES5242880At least 1 — the body cap of fetch() and download().
fetch.timeoutSAFE_FETCH_TIMEOUT301–600 seconds — the budget of fetch() and download(), every hop included.
resolver—SystemHostResolver::classAny instantiable class implementing Contracts\HostResolver; anything else throws when resolved.

Strict reading

  • Every key is read strictly: a wrong type or a value out of range throws InvalidConfigurationException naming the key, instead of falling back to something looser.
  • Blank (a host’s KEY=) means not set and takes the default.
  • Env booleans accept true/false, 1/0, on/off and yes/no. Lists take an array or an env comma list.
  • Settings that would switch the protection off entirely are refused — 0.0.0.0/0 and ::/0 in ranges.allowed, or an IPv6 prefix carrying all of IPv4 (::ffff:0:0/96, 2002::/16).
  • There is no global on/off switch. Code that needs one keeps its own setting and simply does not call the guard.

Environment variables

SAFE_FETCH_ALLOW_HTTP=false
SAFE_FETCH_REDIRECTS=false
SAFE_FETCH_MAX_REDIRECTS=3
SAFE_FETCH_MAX_BYTES=5242880
SAFE_FETCH_TIMEOUT=30

The app’s own hosts

The host of app.url always counts as the app — read as https when it has no scheme (myapp.example, myapp.example:8080). An app.url written as an address (https://52.10.20.30) is refused as app_host too, in any written form. Another public name for the same IP is not recognised: list it in app_hosts.extra.

Translations

resources/lang/{en,sk}/messages.php holds one line per reason (status takes :status) and validation.php the safe_url message (with :attribute). Publish them with --tag=safe-fetch-translations to reword them.

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 crypto

By 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.