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.
Architecture fit. security-core is a thin adapter over voku/anti-xss, positioned at exactly one place in our architecture: the trust boundary where untrusted HTML is either accepted at ingest or rendered as raw markup. It is deliberately not a platform component — the entire public surface is clean(), isClean(), a constructor config array (evil), and a handful of setters forwarding to the underlying sanitizer. Architecturally it should sit behind an internal HtmlSanitizer interface so it reads as a swappable hygiene control rather than a dependency the product is built on. That framing matters more than the code: it determines whether we can replace it in hours or weeks when requirements tighten.
Integration feasibility. Low effort. Composer install, one container binding, and a FormRequest::prepareForValidation() hook gets a working control in a day. The important integration decision is packaging, not code: in a Laravel app we install graham-campbell/security (service provider, facade, config) and consume Core transitively; Core is only a direct dependency for non-Laravel wiring, CLI tools, or package-level reuse. The PHP 8.1 floor introduced in v5.0.0 is the only hard prerequisite and is a platform question, not an engineering one.
Technical risk.
anti-xss strips known-bad patterns (script tags, javascript: URIs, event handlers). It will not catch novel obfuscation, and it gives no per-tag/per-attribute policy control. For high-value rich text (CMS bodies, anything editors touch) it is insufficient on its own.clean() is not reliably idempotent. Sanitizing in two places (middleware plus a model mutator) yields path-dependent stored values. This is a design constraint we must document and enforce, not a bug we can wait out.voku/anti-xss: sanitizer behavior shifts between minor versions. Both need deliberate pinning and a re-test gate.Key questions for the team.
Verdict. Adopt as a cheap, contained control for generic untrusted-HTML cleanup — behind an interface, at a single boundary, with pinned versions. Do not adopt it as the sanitization strategy for rich-text content.
Stack fit. Laravel 9/10/11 on PHP 8.1+ is the primary target, consumed via graham-campbell/security. The framework-agnostic core also serves console commands, importers, and queue workers, which is where a non-HTTP sanitization need usually surfaces first. Everything renders through Blade {{ }} and a strict CSP as primary controls — this package is a complement, never a substitute, and it does not do contextual encoding for attribute, URL, JS, or CSS contexts.
Migration path. There is usually nothing to migrate from except hand-rolled regex filtering, which is a net improvement wherever it exists. The realistic path:
HtmlSanitizer interface with a sanitize(string $html): string contract.SecurityCoreSanitizer adapter that delegates to Security::clean(). Nothing else in the codebase references the vendor type.security.comments.evil vs security.pages.evil) so operations can tune the evil list without a deploy.Compatibility. Pin graham-campbell/security-core to ^5.0 and pin voku/anti-xss explicitly rather than floating it under the wrapper. Confirm the Laravel sibling (graham-campbell/security) has caught up to the same PHP constraint before bumping either — a mismatch here is the most likely source of a broken composer update. Run composer why-not php 8.1 before any major bump. If a service must stay on PHP 8.0, pin ^4.0 with a named owner and a review date; a silent pin is worse than a deliberate one.
Sequencing.
clean() is invoked, so controllers stay decoupled.clean(), so over-stripping is diagnosable in minutes.Maintenance. Low but non-zero, and it recurs. Two packages to track (security-core and voku/anti-xss), with upgrades gated on re-running the payload corpus. Behavior changes arrive on minor versions of the underlying sanitizer, so automated dependency bumps must not auto-merge. Budget a small recurring maintenance line rather than treating this as install-and-forget. The single-maintainer profile means we should assume a future where the package goes quiet and we either fork the thin wrapper or swap the adapter — both are hours of work only if the interface exists.
Support. No vendor. No SLA. No security response process we can cite to an auditor. Community issues only. This means internal ownership is mandatory: a named owner for the pin, the corpus, and the fallback plan, recorded in the dependency register. Auditors should be shown the interface, the corpus, and the documented boundary decision — not the package name.
Scaling. Computationally cheap relative to request cost; regex passes over submitted HTML are not a throughput concern at typical UGC volumes. Two scaling caveats: (1) for bulk imports and queue jobs, avoid per-row instantiation and avoid sanitizing fields that never render as HTML; (2) large rich-text payloads multiply the cost, which is another reason to scope sanitization to fields that actually need it. Applying it broadly adds cost and, worse, manufactures a false sense of a defended boundary. On the product side, "can we ship comments/reviews/forum/tickets this quarter?" becomes a yes rather than a bespoke security project — that's the real scaling win.
Failure modes to plan for.
evil list. Mitigation: debug-level input/output logging and bisecting the evil array; per-field config so one tuning change doesn't affect every surface.composer why-not as a pre-merge check.Ramp-up. Half a day for a mid-level engineer to be productive: the constructor shape, clean()/isClean(), and the evil config array are the whole learning curve. The non-obvious parts are the ones that bite in production — idempotency, the blacklist (not allowlist) semantics, and the two-package split between Core and the Laravel wrapper. The genuine investment is the payload corpus and the boundary documentation, which is where a TPM should place the acceptance criteria rather than on the install itself.
How can I help you explore Laravel packages today?