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 Core Laravel Package

graham-campbell/security-core

Core security utilities for Laravel, providing a lightweight foundation for sanitizing user input, escaping output, and defending against XSS. Powers the graham-campbell/security package and can be used standalone in any PHP or Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the right package. graham-campbell/security-core is the framework-agnostic engine. In a Laravel app, install the wrapper instead and get Core transitively:

composer require graham-campbell/security   # Laravel: provider, facade, config
composer require graham-campbell/security-core  # only for manual wiring / non-Laravel

Confirm your PHP floor. The current major (v5.0.0) dropped PHP 8.0 and requires PHP 8.1+. Run composer why-not php 8.1 before bumping; if a service or CI matrix entry is still on 8.0, upgrade the runtime first or pin ^4.0 with an owner and a review date.

Read exactly two things first. The Security constructor (it takes a config array whose evil key is the list of tags/strings to strip) and the clean() / isClean() pair. Everything else is a thin forward to voku/anti-xss; skim that library's AntiXSS class once so you know what knobs exist behind the wrapper.

use GrahamCampbell\SecurityCore\Security;

$security = new Security([
    'evil' => ['script', 'iframe', 'object', 'embed', 'applet', 'meta', 'style', 'link', 'base', 'form'],
]);

$clean = $security->clean($untrustedHtml);   // rewrites
$ok    = $security->isClean($untrustedHtml); // boolean

First use case. A user-submitted field that you render with {!! !!} — comments, reviews, forum posts, support tickets, CMS/WYSIWYG bodies, profile bios. That's the decision point this package serves. It is not a substitute for Blade's {{ }} escaping, contextual output encoding, or CSP; it exists for the case where you've already decided some HTML must survive to the browser.

Implementation Patterns

Bind it once in the container. A five-line provider buys you type-hinting, injection, and easy faking in tests:

// app/Providers/SecurityServiceProvider.php
$this->app->singleton(Security::class, fn ($app) =>
    new Security($app['config']->get('security.evil', []))
);

If you're using the Laravel security sibling package, this is already done — inject Security and skip the boilerplate.

Sanitize on write, or on render — pick one and document it. Cleaning in the request layer keeps stored data deterministic and keeps the cost off every read path:

protected function prepareForValidation(): void
{
    if ($this->has('body')) {
        $this->merge(['body' => app(Security::class)->clean($this->input('body'))]);
    }
}

Doing both invites double-cleaning, and clean() is not reliably idempotent (see Gotchas). Whatever you choose, put it in the code review checklist so a later middleware doesn't quietly add the second pass.

Keep the evil list in config, and scope it per field. A comment box and a CMS page body don't tolerate the same markup. Per-field lists let ops tune without a deploy:

// config/security.php
return [
    'evil'     => ['script', 'iframe', 'object', 'embed', 'meta', 'style', 'link', 'base', 'form'],
    'comments' => ['evil' => ['script', 'iframe', 'object', 'embed', 'style']],
    'pages'    => ['evil' => ['script', 'iframe', 'object']],
];

Build a small factory/helper that constructs a Security instance from a named profile rather than scattering new Security([...]) through controllers.

Pair isClean() with validation when the policy is "reject", not "rewrite". clean() silently mangles input; sometimes a 422 is the correct response:

'body' => ['required', 'string', function ($attr, $value, $fail) {
    if (! app(Security::class)->isClean($value)) {
        $fail('The body contains disallowed HTML.');
    }
}],

Rewriting is friendlier for users, rejecting is friendlier for auditability. Decide per surface.

Treat it as one stage in a pipeline, not the pipeline. A sane order: schema validation → clean() → policy purifier for high-value content when requirements harden → persist → render with contextual escaping and CSP. anti-xss is blacklist/regex-driven; a whitelist purifier is what gives you per-tag, per-attribute control.

Wrap it behind one internal interface on day one. The cost of abstracting now is minutes; the cost of swapping later without it is days:

interface HtmlSanitizer { public function sanitize(string $html): string; }

final class AntiXssSanitizer implements HtmlSanitizer
{
    public function __construct(private Security $security) {}

    public function sanitize(string $html): string
    {
        return $this->security->clean($html);
    }
}

Test it two ways. In feature tests, bind a fake HtmlSanitizer and assert sanitize() was called — keeps controllers decoupled from the library. In unit tests, run the real instance against a checked-in corpus of known XSS payloads so every dependency bump has a regression gate.

It works fine outside HTTP. Console commands, queue jobs, and bulk importers can call it directly. For large imports, don't instantiate per row and don't sanitize fields that never render as HTML.

Don't put it in an Eloquent mutator and a FormRequest. Pick the boundary. Model mutators fire on seeding, imports, and updateOrCreate() paths you didn't think about, which is exactly how you end up with two different stored shapes for the same input.

Gotchas and Tips

  • No facade, no auto-discovered provider. If you expect Security::clean() to resolve in a Laravel app, you're thinking of graham-campbell/security. Core is the engine; wire it yourself or take the wrapper.
  • Blacklist, not whitelist. It strips known-bad patterns — <script>, javascript: URIs, on* handlers. Novel obfuscation can slip through. Never let it be the only control on high-value content.
  • Constructor shape matters. new Security(['evil' => [...]]) — the array is keyed, not a bare list of tags. A flat array is a common first-attempt bug that silently does nothing useful.
  • Tuning is done on the underlying AntiXSS instance. setAllowedTags(), setAllowedAttributes(), setAllowedPlainAttributes(), setSanitizeNaughtyHtml() and friends live on the wrapped object. Set them before clean() and check return types rather than assuming fluent chaining.
  • clean() is not guaranteed idempotent. A second pass can strip markup the first pass produced. Two enforcement points (middleware + mutator) produce values that differ by code path and are miserable to debug.
  • Encoding is where regex sanitizers get hurt. Double-encoded entities (&amp;lt;script&amp;gt;) and mixed charsets can pass through. Be careful with premature html_entity_decode() — decode too early and you manufacture the injection you were filtering. Sensitive surfaces need a purifier.
  • "It ate my content." Log input and output around clean() at debug level, then bisect the evil array one entry at a time. The culprit is almost always a tag sitting on both the allowlist and the evil list.
  • Don't sanitize everything. Plain strings rendered through {{ }} need none of this. Blanket application adds latency and creates a false sense of where the trust boundary actually is.
  • Pin voku/anti-xss and bump it in isolation. Sanitizer behaviour shifts between minor versions; re-run your payload corpus before merging the bump, never bundled with unrelated upgrades.
  • Majors here ripple. v5.0.0 is "only" a PHP-floor drop, but a major still reshuffles the transitive constraint graph. Check that the Laravel wrapper has caught up before you bump either side.
  • Dependency risk is real and should be recorded. Single maintainer, effectively zero dependents, low community signal. Mitigate with version pinning, the internal HtmlSanitizer interface, an in-repo payload corpus, and a documented fallback (HTMLPurifier / Symfony HtmlSanitizer) in your dependency register. This is a swappable hygiene control, not a platform bet — price it that way.
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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route