Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Safeurl Laravel Package

jaybizzle/safeurl

jaybizzle/safeurl is a lightweight PHP package for validating and sanitizing URLs before use, helping prevent open redirects, SSRF, and malicious links by checking schemes, hosts, and allowed domains. Useful for safe redirects and URL handling.

View on GitHub
Deep Wiki
Context7

Getting Started

Safeurl is a small, focused package: it answers one question — "is this URL safe to follow, redirect to, or store?" — by combining a local blacklist with a remote reputation lookup (Google Safe Browsing). Treat it as a validation primitive, not a security boundary.

Install and configure

composer require jaybizzle/safeurl
php artisan vendor:publish --provider="JayBizzle\Safeurl\SafeurlServiceProvider" --tag=config

Add the API key to .env (never to config/safeurl.php directly):

SAFEURL_GOOGLE_API_KEY=your-key-here

Then clear config cache — this is the single most common reason a "why is it still using the old key" question happens:

php artisan config:clear

Where to look first

  1. config/safeurl.php — the only config surface; note the API key name, cache settings, and any blacklist/whitelist arrays.
  2. vendor/jaybizzle/safeurl/src/ — the facade accessor and the check method. The public surface is tiny (Safeurl::isSafe($url) returning a boolean); read the source once to confirm the exact return shape and exception behaviour in the version you installed, since error handling is not documented in depth.
  3. The package's own test suite — the fastest way to see which URL formats are normalised and which are rejected out of hand.

First use case

The highest-value first integration is a form request guarding a user-supplied URL (webhook endpoint, avatar/logo URL, RSS feed, redirect target):

public function rules(): array
{
    return [
        'website' => ['required', 'url', function ($attribute, $value, $fail) {
            if (! Safeurl::isSafe($value)) {
                $fail('That URL has been flagged as unsafe.');
            }
        }],
    ];
}

Ship that, watch the logs for a week, and only then consider extending it to middleware or bulk URL processing.

Implementation Patterns

Wrap it in your own service, always. Never call the facade directly from controllers, jobs, or validation rules. The package's check is network-bound and can throw; a thin application-level wrapper gives you caching, timeouts, logging, and a single place to swap the implementation for a fake in tests.

final class UrlSafetyService
{
    public function isSafe(string $url): bool
    {
        $key = 'safeurl:'.sha1($url);

        return Cache::remember($key, now()->addHours(12), function () use ($url) {
            try {
                return Safeurl::isSafe($url);
            } catch (\Throwable $e) {
                report($e);

                return false; // fail closed for user-facing redirects
            }
        });
    }
}

Note the explicit fail-closed/fail-open decision. Pick one per call site and document it — an outage at the reputation provider should not silently make every URL "safe" in a redirect flow, nor should it lock users out of a low-risk feature.

Cache aggressively, keyed on the normalised URL. Reputation data changes slowly, and the remote API is rate-limited. Normalise first (lowercase host, strip default ports, strip fragments, drop tracking query params) so that Example.com, example.com/, and example.com#top share one cache entry.

Validate the URL before you check it. Run Laravel's url rule or filter_var($url, FILTER_VALIDATE_URL) first. Don't spend API quota on strings that are not URLs, and be aware that a reputation lookup only sees the URL — it does not protect against open-redirect abuse of your own domain, SSRF to internal IPs, or javascript:/data: schemes. Pair Safeurl with a scheme allowlist (http, https only) and, for outbound fetches, an IP-range guard.

Outbound-work pattern. Anywhere you fetch or display a remote URL, gate it:

  • Redirects — check the target before redirect()->away($url), not after.
  • Queued ingestion — check inside the job, then persist a url_checked_at and url_is_safe column so you can re-check on a schedule rather than on every render.
  • Moderation queues — check on submission, but let a human override flagged links; false positives on shared hosting and URL shorteners are routine.

Scheduled re-checks. Reputation is time-sensitive. A nightly command that re-checks rows older than N days, dispatched as chunked jobs with a rate limiter, keeps stored links honest without blocking requests.

RateLimiter::for('safeurl', fn () => Limit::perMinute(30));

Testing. Bind a fake of your UrlSafetyService in the container rather than mocking the facade:

$this->mock(UrlSafetyService::class, fn ($m) => $m->shouldReceive('isSafe')->andReturn(true));

If you must test the package boundary itself, use Http::fake() — the lookup is HTTP-based — and assert the timeout path by faking a ConnectException.

Gotchas and Tips

Laravel 13 support (v1.2.0). This release is a composer constraint change only: illuminate/support ^13.0 is now allowed. No API changes, no deprecations, no migration steps. If you are planning a Laravel 13 upgrade, Safeurl is no longer a blocker — but it was not re-released with any behavioural change, so treat the package as frozen-feature and pin it deliberately (^1.2) rather than floating to *.

The check is a network call — never do it in a render loop. A page listing 50 user-submitted links will make 50 sequential HTTP requests unless you cache. That is a multi-second TTFB and a rate-limit ban waiting to happen. Always cache, always queue bulk work.

Config caching bites. env() calls inside config/safeurl.php return null once php artisan config:cache has run. If the API key mysteriously stops working after a deploy, that is why. Reference config('safeurl.…') in your code, and re-run config:cache after changing the env.

Fail-closed vs fail-open is your decision, not the package's. The package will either throw or return a value depending on the failure mode (network error vs. a valid "no threats found" response). Do not assume a falsy return means "malicious" — distinguish "known bad" from "could not determine" if your UX depends on it, and log the difference.

Empty or malformed input. Test '', 'not a url', 'http://', and very long URLs against your wrapper. Decide up front whether an unparseable string is false (reject) or an exception, and be consistent across call sites.

URL normalisation and evasions. Reputation lookups match on the string you send. Percent-encoding, IDN/punycode (xn--), trailing dots on hostnames, and user:pass@ prefixes all change what the provider sees. Normalise to a canonical form before checking and before storing, otherwise you will cache near-duplicates and create inconsistent results.

False positives are real. URL shorteners, shared hosting, and CDN domains get flagged regularly because some path on the domain was abused. Do not hard-reject on a single lookup for anything user-facing — build an override path, or downgrade the result to a warning plus moderation.

Not a substitute for SSRF protection. A URL can be perfectly safe by reputation and still resolve to 127.0.0.1 or 169.254.169.254. For any server-side fetch, block private and link-local ranges after DNS resolution; Safeurl has no role there.

Extension points. The package is thin, so extend at the boundary: your own service class, your own validation rule, your own cache store and TTL. Avoid subclassing the package internals — the surface is small enough that a fork or a wrapper is cleaner than monkey-patching, and it keeps upgrade risk near zero.

Before deploying an upgrade. Run your test suite against a real API key in a staging environment once, because Http::fake() will happily hide a changed endpoint or auth header. Given the size of the v1.2.0 diff, a smoke test of one safe and one known-bad URL is sufficient.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource