stevebauman/purify
A Laravel wrapper for HTMLPurifier that sanitizes user input. Clean strings or arrays of HTML through a simple Purify facade, with publishable configuration, caching, and custom profiles for flexible, safe markup filtering.
Architecture fit. Purify is a thin, idiomatic Laravel wrapper around ezyang/htmlpurifier, the long-standing PHP HTML sanitizer. It contributes three things a TPM should care about: a service provider + facade (Purify::clean()), a named-configuration model (configs.default, configs.comments, etc.) that mirrors Laravel's database/mail conventions, and Eloquent casts (PurifyHtmlOnGet / PurifyHtmlOnSet) that let sanitization be enforced at the model boundary rather than scattered across controllers. For a Laravel app that accepts user-authored HTML (CMS bodies, comments, rich-text editor output, imported email/feed content), this is a very small architectural surface: one Composer dependency, one config file, one optional cache path. It does not introduce queues, middleware, new storage, or a service boundary. Fit is strongest for apps already committed to Eloquent as the read path for rendered HTML; it is weaker for APIs that emit JSON consumed by a separate rendering tier, where sanitization must be applied explicitly before serialization.
Integration feasibility. Low effort. composer require stevebauman/purify, publish the provider config, then either call the facade at the boundary or attach a cast. The README documents the two supported enforcement points (on read, recommended; on write, optional) and ships ready-made definitions for HTML5 and Basecamp Trix, which removes the most common blocker (sanitizer stripping editor-specific markup). Named configs mean one package can serve multiple trust levels — e.g. a permissive posts config and a locked-down comments config — without forking code. Dynamic per-call overrides exist but are explicitly not merged with the default, which is a footgun worth encoding in team conventions. Estimated effort for a first production slice: 1–3 engineering days including tests; per-additional-config work is hours, not days.
Technical risk. Concentrated in three areas:
| Risk | Severity | Notes |
|---|---|---|
| Upstream HTMLPurifier posture | Medium–High | The underlying library is mature but effectively feature-frozen and slow-moving relative to PHP/Laravel release cadence. The wrapper inherits that trajectory. |
| Sanitizer bypass via permissive definitions | High | Custom Definition/CssDefinition classes are the intended extension point and also the primary way teams reintroduce XSS (widening data-*, contenteditable, style). Any change here is a security review item, not a routine PR. |
| Silent content mutation | Medium | Purifier strips rather than rejects. Legitimate markup disappearing after a config change is a user-visible regression that surfaces as bug reports, not alerts. |
| Bus factor | Medium | Single-maintainer wrapper; the README's upgrade guides show the wrapper has absorbed breaking upstream changes before, which is good, but the release cadence depends on one person. |
| Signal quality of the metadata | Low confidence | 537 stars with 0 listed dependents and a score of ~20 sits oddly against a package this commonly used; treat the popularity/opportunity scores as unreliable inputs and validate adoption independently. |
Key questions to resolve before committing.
ezyang/htmlpurifier maintenance commitment, and does the wrapper pin a version range that will accept PHP 8.4 / Laravel 12+?Stack fit. Requires PHP ≥ 7.4 and Laravel ≥ 7.0 — trivially satisfied by any maintained Laravel application. No database migrations, no queue workers, no frontend coupling. The optional serializer cache uses either the filesystem or a Laravel cache store, so it slots into existing config/filesystems.php and config/cache.php conventions rather than inventing new infrastructure. This is a "composer require and go" package, not an integration project.
Migration path. Because the recommended practice is sanitize-on-output, no data migration is required. Existing rows stay as-is; the purifier becomes a rendering-time filter. That is the single most important sequencing fact: it decouples the security fix from a backfill, and it means rollback is a config revert rather than a data restore.
Suggested sequence:
configs.default to a deliberately restrictive allowlist; ship it behind a feature flag or config toggle so it can be disabled in one deploy.PurifyHtmlOnGet on the identified models (or a shared base/trait), keeping the facade available for non-Eloquent paths.comments, posts, emails) instead of one global rule set.php artisan purify:clear into the deploy pipeline only when definitions change.Compatibility and upgrade posture. The package has absorbed upstream breaking changes across major versions (v4→v5→v6), and the documented upgrade guides cover the config-reshaping that v5 introduced. Plan for an annual upgrade review tied to Laravel's major release cycle, and pin the version explicitly (^6.x) rather than floating. Watch for the deprecated settings array pattern if any legacy install is being brought forward — that shape was replaced by configs.
Sequencing risk to call out. Custom definitions should land after the baseline is in production and measured. Shipping a permissive Trix definition and a strict default in the same release makes it impossible to attribute stripping regressions to either change.
Maintenance. The wrapper is small and low-churn; expect dependency-bump PRs rather than feature work. The real maintenance load is configuration maintenance: allowlists, custom definitions, and CSS definitions are security-relevant code that drifts as product requirements change (a new editor feature, an embedded video, a text-align: start value not in the default CSS definition). Budget for a named owner of config/purify.php and any Definitions/* classes, plus a review gate on changes to those files — equivalent to how you'd treat auth config.
Support. Community-supported, MIT, single primary maintainer. There is no vendor SLA. Internal mitigation: keep the integration shallow enough that replacing the wrapper with direct HTMLPurifier calls (or with a different sanitizer entirely) is a contained change. The facade and casts make this realistically a day or two of work, which is the right amount of coupling for a security-critical dependency. Upstream HTMLPurifier issues are the ones most likely to block you, not wrapper issues.
Scaling. Sanitization is CPU-bound string parsing, executed per rendered value. Two distinct cost profiles:
purify:clear never flushes unrelated application cache. The README flags exactly this hazard.Failure modes to design for.
| Failure | Symptom | Mitigation |
|---|---|---|
| Serializer cache stale after definition change | Newly allowed tags still stripped | Run purify:clear in the deploy step whenever definitions/css-definitions change |
| Cache path not writable / disk full | Exceptions on first render after deploy | Dedicated path with permissions checked in deploy smoke test |
| Over-permissive definition | XSS in stored content | Security review gate + adversarial test corpus per definition |
| Over-restrictive default | Legitimate markup silently dropped | Strip-rate logging, before/after diff on a real content sample |
Dynamic config() not merged with defaults |
Rules unexpectedly absent | Team convention: use named configs, reserve config() for throwaway cases |
| Sanitizing on both write and read | Content degradation over successive saves | Pick one enforcement point per field and document it |
| Malformed/invalid UTF-8 input | Purifier drops or mangles content | Validate encoding at the input boundary |
Ramp-up. A Laravel developer can be productive in under an hour: facade call, one cast, done. The skill gradient is entirely in the definition layer — HTMLPurifier's HTML.Allowed, CSS.AllowedProperties, and the HTMLPurifier_HTMLDefinition API are idiosyncratic and thinly documented outside the upstream config reference. Plan for one engineer to become the internal definition owner, and pair that with a fixture-based test suite so the knowledge is encoded rather than personal. Given the low integration cost and the alternative (hand-rolled regex sanitization, which is reliably worse), the build-vs-buy calculus favors adopting this package for any Laravel app that renders user HTML — with the caveat that you are adopting HTMLPurifier's security model and lifecycle, and should validate its upstream trajectory as part of the decision rather than treating the wrapper's star count as the signal.
How can I help you explore Laravel packages today?