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

Security Laravel Package

artisanpack-ui/security

Core Laravel security toolkit for ArtisanPack UI: input sanitization, output escaping, KSES filtering, validation rules, security headers, XSS protection, rate limiting, and CSP with nonces, presets, and violation reporting, plus audit, scan, and benchmark Artisan commands.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config first — the config file is the real documentation.

composer require artisanpack-ui/security
php artisan vendor:publish --tag=security-config

Before writing any code, open config/security.php. It defines the sanitizer defaults, the KSES allowed-tag set, the CSP presets and reporting toggle, and the named rate limiters (web, api, login, password_reset). Nearly every "why is this stripping my tags / blocking my script" question is answered in that file, not in the README.

Confirm you're on 2.x and read the scope fence. The 2.0 release split the old monolith: authentication, 2FA, RBAC, uploads, analytics, and compliance are now sibling packages (security-auth, rbac, secure-uploads, security-analytics, compliance, …). If you came here expecting a full security suite, install artisanpack-ui/security-full instead. This package is deliberately core-only: strings in, strings out, plus headers.

Where to look first:

Question Look at
Which method do I call for X? docs/api-reference.md
Am I using this correctly? docs/security-guidelines.md
Why is my config doing that? config/security.php (published)
Coming from 1.x? UPGRADE.md

First use case — the 15-minute win. Register the global security headers and start escaping output, which is where most Laravel XSS actually lives:

// bootstrap/app.php (Laravel 11+) or Kernel.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\ArtisanPackUI\Security\Http\Middleware\SecurityHeaders::class);
    $middleware->alias([
        'csp'             => \ArtisanPackUI\Security\Http\Middleware\Csp::class,
        'security.headers'=> \ArtisanPackUI\Security\Http\Middleware\SecurityHeaders::class,
        'xss.protection'  => \ArtisanPackUI\Security\Http\Middleware\XssProtection::class,
        'api.security'    => \ArtisanPackUI\Security\Http\Middleware\ApiSecurity::class,
        'api.rate_limit'  => \ArtisanPackUI\Security\Http\Middleware\ApiRateLimit::class,
    ]);
})

Then in views where you must render stored HTML, filter with KSES rather than raw {!! !!}:

{!! kses($post->body) !!}

And for everything else, let the escaper do the contextual work:

use ArtisanPackUI\Security\Facades\Security;

echo Security::escHtml($comment->body);
echo Security::escAttr($user->display_name);
echo Security::escUrl($link->href);

Verify with php artisan security:test-headers and php artisan security:check-config before you build anything on top.


Implementation Patterns

Sanitize at the boundary, escape at the output — never one instead of the other

The single most common integration mistake is treating these as interchangeable. They aren't:

  • Sanitizers (sanitizeEmail, sanitizeUrl, sanitizeText, sanitizeInt, sanitizeArray, …) normalize stored input.
  • Escapers (escHtml, escAttr, escUrl, escJs, escCss) make output safe for a specific context.

A field sanitized as text can still be dangerous if you later drop it into an onclick attribute. Escape at the point of rendering, always matching the context.

FormRequests: validate for rejection, sanitize for normalization

class StoreCommentRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'    => ['required', 'string', 'max:120', new \ArtisanPackUI\Security\Rules\NoHtml],
            'website' => ['nullable', new \ArtisanPackUI\Security\Rules\SecureUrl],
            'body'    => ['required', 'string', 'max:5000'],
        ];
    }

    protected function prepareForValidation(): void
    {
        $this->merge([
            'name'    => sanitizeText($this->input('name')),
            'website' => sanitizeUrl($this->input('website')),
        ]);
    }
}

NoHtml and SecureUrl reject (good UX, clear error messages). The sanitize* helpers normalize (silent, lossy). Use validation for anything the user should be told about, sanitization for the messy-but-acceptable.

Rich text: pick a storage strategy and stick to it

Two viable patterns:

  1. Filter on write — run kses() before saving. Cheaper on read, but you can never loosen the tag policy retroactively without backfilling.
  2. Store raw, filter on render — {!! kses($post->body) !!}. Lets you tighten policy instantly, costs CPU per render (cache the rendered fragment).

For anything user-editable and long-lived, pattern 2 is usually the better trade. Keep kses() off the escape path entirely — it emits real HTML by design, so never wrap it in escHtml().

Middleware composition by route group

Don't put csp on JSON endpoints; do put it on anything returning HTML.

Route::middleware(['web', 'security.headers', 'csp', 'xss.protection'])->group(function () {
    // HTML
});

Route::middleware(['api.security', 'api.rate_limit:api'])->prefix('api')->group(function () {
    // JSON
});

Route::middleware(['throttle:login'])->post('/login', LoginController::class);

The named limiters (web, api, login, password_reset) read their thresholds from config — tune them there rather than hardcoding numbers in routes.

CSP: roll out in three phases, don't start at Strict

// Phase 1 — observe. Report-only + violations stored.
// config/security.php
'csp' => [
    'preset' => 'relaxed',
    'report_only' => true,
    'reporting' => ['storeViolations' => true],
],
<script @csp_nonce>
    window.__BOOTSTRAP__ = @json($bootstrap);
</script>

Then:

  1. Observe for a week. php artisan csp:stats shows what's being blocked. The reporting endpoint (and the optional csp-dashboard Livewire component) gives you the violating directives with URIs and samples.
  2. Enforce — flip report_only to false, keep the violations endpoint live so legitimate breakage is visible immediately.
  3. Tighten — switch to the strict preset (or livewire if you're running Livewire), and schedule php artisan csp:prune to keep the violations table from growing unbounded.

Every inline <script> and <style> needs @csp_nonce. Miss one and Strict will silently kill it — that's the number-one CSP support ticket.

Use the hooks to centralize cross-cutting policy

The hook layer is the right place for "we have one rule and it applies everywhere," instead of chasing every call site.

// One allowlist for every escaped URL in the app.
addFilter('ap.security.escapedOutput', function (string $value, string $context, string $original): string {
    if ($context !== 'url') {
        return $value;
    }
    return app(UrlAllowlist::class)->passes($original) ? $value : '#blocked';
});

// Add an analytics domain only on marketing routes.
addFilter('ap.security.csp.directives', function (array $directives, Request $request): array {
    if ($request->is('pricing', 'blog/*')) {
        $directives['script-src'][] = 'https://plausible.io';
    }
    return $directives;
});

// Route violations into your alerting rather than only the DB.
addAction('ap.security.csp.violationHandled', function (CspViolationReport $report): void {
    SecurityAlerts::dispatch($report);
});

ap.security.sanitizedInput also lets you build an audit trail: a subscriber that logs ($original, $value, $type) whenever the result differs is a cheap way to spot form fields where the sanitizer is mangling legitimate user data.

Put the audit commands in CI

The testing toolkit ships scanners and machine-readable reports (JSON, HTML, JUnit, SARIF, Markdown). A practical gate:

- run: php artisan security:baseline        # establish the accepted state once
- run: php artisan security:scan --format=sarif > security.sarif
- run: php artisan security:scan-deps
- run: php artisan security:check-config
- run: php artisan security:audit --fail-on=high

Upload the SARIF to GitHub code scanning and it shows up as annotations on the PR — that's the highest-leverage use of this package in a real workflow. security:benchmark is worth running once so you know what the sanitizers cost on your hot paths (e.g. kses() on a list of 50 comments).


Gotchas and Tips

Pin ^2.1.0 minimum. Two real regressions shipped nearby:

  • 2.0.1 fixed a packaging bug where an over-broad export-ignore (/src/**/*Test.php) stripped CspTest from the tarball, so every consumer got Target class [...\CspTest] does not exist during package discovery. If you see that error, you're on 2.0.0.
  • 2.1.0 fixed sanitizeFloat(), which was declared float but returned number_format()'s string — a TypeError under strict_types=1 on every call. If you use strict_types (you should), do not use sanitizeFloat() on 2.0.x.

The ksesAllowedTags hook only fires when you call kses() with the default $config = 1. Pass an explicit config and the hook is bypassed by design ("caller intent isn't silently overridden"). This is a real footgun: teams register the filter, then a developer adds a kses($html, 5) call for a special case, and now that one call site ignores the app-wide policy. Grep for non-default kses( calls during review.

sanitizeArray fires ap.security.sanitizedInput twice per element — once as text, then once as array on the whole result. A logging subscriber will double-count; a mutating subscriber can compound. Make subscribers idempotent and check $type explicitly.

The hooks can weaken every guarantee the package makes. ap.security.sanitizedInput and ap.security.escapedOutput receive the already-safe value and may return anything — including the original. A single permissive subscriber (or a third-party package registering one) turns off escaping app-wide. Treat addFilter/addAction registrations as security-critical code: they belong in a reviewable, tested service provider, never in a Blade file or a package you don't control. Prefer subscribers that narrow (further sanitize, allowlist) over ones that broaden.

Don't double-escape. escHtml() followed by Blade's {{ }} shows literal &amp;lt; to users. Either escape in PHP and use {!! !!}, or skip escHtml() and let Blade's auto-escaping do it. For attribute contexts, Blade's {{ }} is not sufficient for unquoted attributes or href/src — that's what escAttr and escUrl are for.

Escaping is not validation. escUrl() makes a URL safe to emit; SecureUrl decides whether it's a URL you're willing to store. javascript: schemes and open redirects are validation problems, and the URL allowlist filter pattern above is the enforcement point most apps actually need.

CSP nonces and HTML caching are incompatible. A nonce is per-request; a cached page carries a stale nonce and Strict CSP blocks every inline script on it. If you cache rendered fragments, either exclude nonce-bearing partials from the cache or move the inline data into a nonce-free mechanism (JSON endpoint, data- attributes with an external script).

Livewire is an optional peer dependency. The csp-dashboard component requires livewire/livewire. Don't pull Livewire into a small app just for the dashboard — csp:stats gives you the same numbers in the terminal. If you do run Livewire, use the livewire preset; the strict preset will fight it.

Command names drifted between the 2.0 alpha changelog and the README (csp:generate vs security:generate-csp). Don't trust either — run php artisan list | grep -E 'csp|security' once and copy the real names into your CI and docs.

Violation storage grows fast on a busy site. storeViolations = true is invaluable during rollout and a liability afterwards. Schedule csp:prune from day one, and consider turning storage off (keeping the reporting endpoint) once the policy is stable — you'll still get the browser console reports without the table.

Rate limiter state leaks across tests. If you use api.rate_limit:api and hit an endpoint in a test loop, you'll get spurious 429s. security:clear-rate-limits exists precisely for this — call it in setUp() or the relevant test teardown.

Small package, young ecosystem. One star, zero dependents means you are effectively an early adopter: fewer StackOverflow answers, less battle-testing, and the changelog is your best bug tracker (both releases above were real breakages). Practical mitigations:

  • Read src/ for anything load-bearing. It's a small surface area.
  • Keep an escape hatch: escHtml is Laminas Escaper underneath and kses() is htmLawed. If this package stalls or a wrapper misbehaves, you can drop to those libraries directly without changing your call sites much.
  • Wrap the facade calls you care about in a thin app-level Sanitizer/Escaper service. That single indirection is what makes the 1.x→2.x-style repackaging (which this project has already done once) a one-file change instead of a grep-and-replace.

Remember php artisan config:cache after editing the published config. Middleware, presets, and rate limits all read from it; a stale cache produces "I changed the preset and nothing happened" confusion.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib