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

Defender Laravel Package

orchid/defender

Orchid Defender is a Laravel package for sanitizing user input and filtering out malicious content. It provides configurable string cleaning helpers to protect applications from XSS and other injection attacks with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify what you're actually installing before you write a line of application code.

The source material is contradictory: the package description advertises HTML/XSS sanitization, while the technical assessments describe an IP/URL/user-agent allow-deny middleware. Do not plan work until you have inspected the code:

composer show orchid/defender
ls vendor/orchid/defender/src
cat vendor/orchid/defender/config/defender.php

Read every file under src/ — with ~0 dependents, the source is the documentation. Note which facade methods exist in your pinned version (addIp, addUrl, addUserAgent, addReferer, addMethod, global) and whether whitelist()/blacklist()/sanitize() are all present. The API has shifted between tags.

Install and publish config.

composer require orchid/defender
php artisan vendor:publish --tag=defender-config

The repo resolves to the theOrchid/Platform monorepo, so treat your Composer constraint as a pin against an Orchid release tag, not a free-floating ^x.0.

Register the middleware narrowly, not globally.

Laravel 10 and below — app/Http/Kernel.php:

protected $middlewareAliases = [
    'defender' => \Orchid\Defender\Middleware\DefenderMiddleware::class,
];

Laravel 11+ — bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'defender' => \Orchid\Defender\Middleware\DefenderMiddleware::class,
    ]);
})

Then apply it to one route group in routes/web.php:

Route::middleware(['defender'])->group(function () {
    Route::get('/admin', ...)->name('admin.index');
});

First use case: staging/pre-prod lockdown by IP allowlist.

This is the lowest-risk, highest-value first application. It has a bounded blast radius (a non-public environment), a clear success signal (only the office/VPN CIDR gets in), and it exercises the exact machinery — client IP resolution, allowlist precedence, config caching — that you will need to validate before touching production. Do not start with customer-facing auth.

Build a throwaway PoC route before you trust anything.

Route::get('/_defender/probe', function (Request $request) {
    return [
        'ip' => $request->ip(),
        'remote' => $request->server('REMOTE_ADDR'),
        'proxies' => $request->getTrustedProxies(),
        'config' => config('defender'),
    ];
});

Hit it through your real CDN/load balancer, not from php artisan serve. If ip() does not return the client address you expect, stop — every rule you write afterwards is wrong.


Implementation Patterns

Allowlist for restricted surfaces (staging, internal tools, break-glass admin).

// app/Providers/AppServiceProvider.php  (or a dedicated DefenderServiceProvider)
use Orchid\Defender\Facades\Defender;

public function boot(): void
{
    if (app()->environment('staging', 'preprod')) {
        Defender::whitelist()
            ->addIp('203.0.113.0/24')   // office
            ->addIp('198.51.100.7')     // VPN egress
            ->global();
    }
}

Keep rules in config/defender.php where possible, fed from .env, so a change is a deploy rather than a code edit.

Denylist for incident response.

Reserve the blacklist for short-lived, tactical blocks: a brute-force source, an abusive scraper. Rules added here should have an owner and an expiry date written down outside the codebase — the package will not remind you to remove them.

Defender::blacklist()->addIp('192.0.2.44')->addUserAgent('BadBot/1.0');

Operator-editable rules in a cached table.

The moment "unblock this customer" requires a deploy, you have a problem. Move dynamic rules to a small table and cache it:

$rules = Cache::remember('defender:rules', now()->addMinutes(5), fn () =>
    DefenderRule::where('active', true)->get()
);

foreach ($rules as $rule) {
    $rule->type === 'allow'
        ? Defender::whitelist()->addIp($rule->value)
        : Defender::blacklist()->addIp($rule->value);
}

Expose a single admin toggle (an Orchid screen if you are on Platform) that flushes defender:rules on save. Always pair with an artisan command — php artisan defender:flush — that works even when the admin UI is the thing being blocked.

Shadow mode before enforcement.

Wrap the real middleware in a log-only variant and run it for a week on real traffic:

class DefenderShadow
{
    public function handle($request, Closure $next)
    {
        // call the same rule evaluation the package uses
        if ($this->wouldBlock($request)) {
            Log::channel('defender')->warning('would-block', [
                'ip' => $request->ip(),
                'url' => $request->fullUrl(),
                'ua' => $request->userAgent(),
            ]);
        }
        return $next($request);
    }
}

Diff the log against real traffic volume. False positives you find here are false positives you don't get paged about at 2am.

Break-glass path outside the middleware group.

Never let the protected surface be the only way to fix the protected surface:

Route::get('/_breakglass', [BreakGlassController::class, 'challenge'])
    ->middleware(['auth', 'signed']);   // deliberately no 'defender'

Make it require a signed URL or a TOTP secret held in your password manager, and test it quarterly.

Route-group scoping over global() in production. Use global() only for environment lockdowns. For anything customer-adjacent, attach the middleware to the specific group and keep the default web and api groups untouched, so a middleware bug cannot take down login.

Testing. Feature tests with an explicit client IP and UA:

it('blocks a blacklisted ip', function () {
    config(['defender.blacklist' => ['192.0.2.44']]);
    $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.44'])
         ->get('/admin')
         ->assertForbidden();
});

it('lets a whitelisted ip through even if blacklisted', function () {
    config([
        'defender.whitelist' => ['192.0.2.0/24'],
        'defender.blacklist' => ['192.0.2.44'],
    ]);
    $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.44'])
         ->get('/admin')
         ->assertOk();
});

it('resolves the client ip behind a trusted proxy', function () {
    $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.5'])
         ->withHeaders(['X-Forwarded-For' => '192.0.2.44'])
         ->get('/admin')->assertForbidden();
});

That second test is the one that matters most — precedence between allow and deny is the single most common source of surprise. Write it against your pinned version and treat it as a regression guard for upgrades.

If the sanitization API is the real one, the pattern is different and simpler: sanitize on render, not on store. Keep the raw source in the database (you will need it when you loosen the config) and run the sanitizer in a Blade component or an accessor. Never sanitize twice, and never sanitize trusted CMS content with the same policy as anonymous user input.


Gotchas and Tips

Client IP resolution is the whole ballgame. Behind Cloudflare, ALB, or nginx, $request->ip() returns the proxy unless TrustProxies is configured with the correct CIDRs and the middleware runs before Defender. Verify with the probe route from the Getting Started section. This is the #1 cause of both mass lockouts and silent bypasses, and it is invisible in local development.

Verify allowlist/denylist precedence in source, not in docs. There are no docs. Read the middleware and write the test.

IPv6 and CIDR are where naive implementations break. 2001:db8::1 will not match a 2001:db8::/32 rule unless CIDR matching is implemented. ::1 in local dev behaves differently from 127.0.0.1. Test both families explicitly.

Decide fail-open vs. fail-closed and write it down. If rules live in cache and the cache is down, does the middleware allow everything or block everything? Both are defensible; picking one by accident is not. For a denylist, fail-open is usually right. For an allowlist on a sensitive surface, fail-closed — but then you must have the break-glass path.

php artisan config:cache will surprise you. Anything calling env() from a service provider rather than from config/*.php returns null in production. Keep every tunable in the config file, and run php artisan config:clear after rule changes during debugging.

Cache propagation across instances. With multiple app servers, Cache::forget() on one node is not enough unless you are on a shared store (Redis/Memcached). Use a shared cache or a short TTL — never per-node file cache for the rule set.

Middleware ordering. Defender must run after TrustProxies and before anything expensive. If you put it inside the web group after StartSession, you are paying session cost on requests you are about to reject. Consider whether it belongs early in the global stack for deny rules.

Monorepo coupling is a real upgrade tax. There is no independent release cadence — bugs are fixed when Orchid ships. Before bumping orchid/platform, diff vendor/orchid/defender and re-run your precedence and proxy tests. Lock the version in composer.lock and have a documented rollback (composer require orchid/defender:x.y.z).

You are the QA team. Zero dependents means no Stack Overflow answers, no issue backlog, and no one else has hit your edge case. Budget for source-level debugging and keep a local fork/patch path available ("orchid/defender": "dev-main" with a VCS repository entry).

Don't let it grow into a firewall. The failure mode for packages like this is scope creep — geo-blocking, threat feeds, per-tenant policy. Every one of those belongs at the edge or in a maintained package. Keep Defender to static allow/deny lists scoped to specific route groups, and push anything dynamic to your CDN or WAF.

Document the uninstall. Because the capability is a thin middleware, removal should be a 10-minute job: drop the alias, delete the service provider call, keep the break-glass route. Write that down before you deploy it, so rollback isn't a research project during an incident.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings