craftcms/url-validator
Validate URLs and IP addresses before opening a connection to guard against SSRF and DNS rebinding. Rejects non-HTTP schemes, private/reserved/loopback addresses, cloud-metadata hosts, and IPv4-embedding IPv6, returning resolved IPs so you can pin the connection.
Architecture fit. The package is a framework-agnostic Composer library, not a Laravel package: no service provider, no publishable config, no facade. That is an advantage for a TPM — adoption is a composer require and a wrapper class, with no framework-version coupling and no upgrade treadmill tied to Laravel majors. Architecturally, though, it imposes a real constraint: the design is validate-then-pin. The validator returns the resolved IP set, and the HTTP client must be pinned to those IPs (e.g. CURLOPT_RESOLVE) for the protection to be meaningful. Validation without pinning is theatre. This means the package only fits workloads where we control the outbound client — our own Guzzle/Http calls, background jobs, and crawlers. It does not retrofit onto third-party SDKs that manage their own connection pools, and it does not protect traffic that goes through a managed egress proxy unless the proxy is doing equivalent checks.
Integration feasibility. Low engineering effort, moderate design effort. The library API is small (validate, validateIp, validateScheme, validateHostname), injectable DNS resolver, and an options array for schemes, disallowed hostnames, IPv4 ranges, and filter flags. The work is not in calling it — it is in the surrounding discipline: a shared wrapper, a form-request validation rule, manual redirect handling (Guzzle follows redirects and re-resolves DNS by default, which reopens the hole on hop two), re-validation inside queued jobs to close the submit-to-execute TOCTOU window, and connection pinning at the client. Expect roughly one engineer-week for a competent platform engineer to build and test a SafeUrlFetcher abstraction, plus per-team integration time to route existing fetches through it. No database, no migrations, no runtime services.
Technical risk. The security logic itself is the credible part: scheme allowlisting, IPv4/IPv6 range checks, IPv4-mapped/NAT64/6to4/Teredo unwrapping, hex and decimal hostname normalization, cloud-metadata domain coverage for AWS/GCP/Kubernetes, and multi-address checking. That is precisely the hard part teams get wrong when hand-rolling, and it is the main argument for adopting rather than building. The risks are around it: (1) Maturity — 1 star, 0 dependents, v1.1.0, README and changelog only. Blast radius of a regression is a security hole, so treat every version bump as a security review, not a routine composer update. (2) Maintenance continuity — published by Craft CMS (Pixel & Tonic), a reputable org, but with no dependents there is no ecosystem pressure keeping the blocklists current; the changelog is the only signal and it currently shows two releases. (3) Coverage gaps by design — metadata domain blocking is heuristic; the durable defence is the IP-range check plus network egress rules. (4) Developer-environment friction — localhost, *.local, 10.x, and Docker service names are rejected by default, which will break local workflows unless a dev-only allowlist is gated behind an environment check that cannot leak into production. (5) False confidence — the biggest risk is a team adopting it and then calling Http::get($url) without pinning.
Key questions.
Stack fit. Fits cleanly into a Laravel codebase as a plain Composer dependency. It should not be sprinkled through controllers; the right shape is a single platform-owned SafeUrlFetcher service (singleton-bound) that composes the validator, the pinned HTTP client, manual redirect handling, and structured logging on rejection. Teams then consume one internal API — a validation rule for form input, and a fetcher method for jobs and services. That keeps the vendor package behind one seam, which matters because swapping or patching it later becomes a one-file change.
Migration path. No migration in the data sense — no schema, no config to publish, no service provider to register. The migration is behavioural: inventory every endpoint and job where a user-supplied URL causes a server-side fetch (webhook registration and delivery, link previews/unfurling, avatar/remote-image import, RSS/feed ingestion, "import from URL" onboarding, PDF/screenshot generation, OAuth/OIDC discovery, sitemap crawlers, any AI browsing tool). Rank by exposure — unauthenticated endpoints first, then authenticated-but-low-trust, then internal admin. Each one gets rerouted through the wrapper; none get a bespoke call to the vendor class. Existing endpoints that already accept URLs are gap-closing candidates and should be tracked as remediation items with an owner and a date, not left as "backlog nice-to-have."
Compatibility. PHP-version and framework compatibility are effectively non-issues given the package is a small, dependency-light library; the real compatibility surface is the HTTP client. Guzzle supports CURLOPT_RESOLVE, so it works. Anything that abstracts away cURL options — higher-level SDK wrappers, some HTTP facades if configured restrictively — needs verification before it is declared in-scope. Note also that a hostname can return both a public A record and a private AAAA record; the pinning strategy must cover the address family the client actually uses, and all returned addresses must be checked, not just the first.
Sequencing.
SafeUrlFetcher plus a SafeUrl validation rule, with dev-only allowlisting and rejection logging baked in.Maintenance. Low ongoing cost in code, non-trivial in attention. The package is small and stable, so there is little to patch; the maintenance burden is the periodic review of the blocklists and options as new cloud providers and metadata endpoints appear, and the discipline of upgrading deliberately rather than automatically. With one star and zero dependents there is no community canary — we are the ones who notice a regression. Budget for a named owner and a quarterly review, plus a policy that any version bump gets a diff read from someone who can reason about SSRF.
Support. Vendor support is essentially "MIT licence and a security policy" — no SLA, no paid tier, no guaranteed response on a disclosed bypass. That is normal for a small security library from a reputable org, but it must be an explicit accepted risk in the security review, not an assumption. Internally, the wrapper owner is the support path: teams file against the wrapper, the platform team triages against the vendor or patches locally. Incident response needs a documented path for "a URL got through" — revoke, patch, audit logs, notify — because the failure mode is silent by default.
Scaling. Computationally negligible: one DNS resolution per validation, which is a rounding error against the outbound HTTP request it guards. The scaling concern is architectural, not throughput. Validate-then-pin couples the fetcher to a specific client and complicates pooling and connection reuse; at high fetch volume the cleaner answer is a dedicated fetch service or egress proxy that performs these checks centrally, with the library as the in-process guard closer to the edge. If fetch volume grows enough that per-request DNS becomes a latency item, the injectable resolver is the lever — plug in a caching resolver, keeping TTLs short enough not to undermine the rebinding defence.
Failure modes. Enumerate them explicitly, because each needs a defined behaviour:
allow_redirects left on, or a hop limit that is too generous. Manual redirect handling with a 3–5 hop cap is the control.10.x, partner hosts resolving to private ranges; surfaced by the log-only phase.Ramp-up. Short for engineers, longer for the organisation. A competent backend engineer will read the README and the src/ directory in under an hour and be productive with the wrapper the same day. The organisational ramp is the real work: getting every team that fetches user URLs to route through one wrapper, changing review habits so unpinned fetches get flagged, and getting security comfortable that the accepted risk around a low-maturity dependency is documented. Realistic estimate: one platform engineer for a week to build and instrument the wrapper, one to two sprints of incremental integration across consuming teams, and roughly a quarter before the pattern is habitual and the log-only phase has been retired. The opportunity score of 55.25 reads correctly here — high strategic value as a guardrail for features we want to ship, low market validation, so the decision is "adopt deliberately behind a wrapper with egress defence as a second layer," not "adopt and forget."
How can I help you explore Laravel packages today?