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

Every name and type that reaches storage is treated as untrusted:

  • The mime type is sniffed from the bytes (ext-fileinfo) — never taken from the client, a remote Content-Type header or a disk’s stored metadata.
  • The stored name is one safe path segment. Directory parts, separators and control characters are stripped from a client’s name and from usingFileName(), so a name can never reach outside the media’s own directory.
  • The extension can’t lie about active content. An extension a web server may execute (.php, .phtml, .shtml, …) is never stored, and one a browser runs (.html, .svg, .js, …) is kept only when the bytes really are that type — a PNG uploaded as x.html is stored as x.png. Truthful or harmless extensions (IMG_0001.JPG, data.csv) are kept.
  • addFromUrl() only fetches http/https URLs, and it is safe to hand a user’s URL — it shares its guard with MediaLibrary::remote() (see Remote URLs). Every hop is canonicalised and rebuilt (user info, numeric IPv4 spellings, a backslash or % in the host and IPv6 zone ids are refused), and a host that is, resolves to or redirects to a private, loopback, link-local (cloud metadata), carrier-grade NAT or other reserved address — IPv4 or IPv6 — or is localhost is refused before any request to it (SSRF). Redirects are followed by hand (media.remote.max_redirects, five by default), each hop vetted the same way, and every host name is resolved once and the connection pinned to every vetted address, bypassing the environment’s proxy, so a second DNS answer can’t swap in an internal one.
  • media.remote.headers go to the URL’s own origin only — a redirect to another origin doesn’t receive them unless media.remote.forward_headers_on_redirect is on — and media.remote.timeout is one budget for the whole download, every hop included.
  • Trusted internal hosts go in media.remote.allowed_private_hosts (exact host names, addresses or address/prefix ranges); media.remote.block_private_networks => false turns the guard off — only for URLs you trust completely. Pinning needs ext-curl: without it a host name is refused, while address literals and allowlisted host names still work. An internationalised or punycode (xn--) host name needs ext-intl; without it, it is refused.
  • A remote download streams to disk and stops the moment it passes the size cap, counting decoded bytes, with or without a Content-Length. Every refusal throws RemoteFileRejected; a non-2xx answer throws FileDoesNotExist, and a transport failure surfaces as Laravel’s ConnectionException.
  • Images are measured from their header before anything is decoded: one over media.max_image_pixels (50 megapixels by default) is stored without variants or placeholders, so a tiny file declaring a huge canvas can’t exhaust memory. SVG is never decoded on the server — see Image variants.
  • A bucket without acceptsMimeTypes() accepts any type, HTML and SVG included. Give every bucket that holds user uploads an allowlist.

Serving is hardened the same way — see URLs, streaming & downloads for which types the streaming route serves inline, and why private media needs a private disk.

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.