vanilla/htmlawed
Laravel integration for HTMLawed, a PHP HTML sanitizer and validator. It cleans user-submitted markup, balances tags, filters unsafe attributes and elements, and helps preserve safe formatting in rich text. Useful for sanitizing comments, CMS content, and other untrusted HTML.
Architecture fit. Unchanged by this release. htmLawed remains a single-file, dependency-free PHP HTML filter, and vanilla/htmlawed remains Vanilla Forums' Composer-packaged fork of it. It is a leaf dependency: no framework coupling, no service container assumptions, no global state beyond a class you instantiate yourself. That makes it trivially embeddable in a Laravel app — but it also means Laravel gives you nothing for free. There is no service provider, no facade, no config publishing, no Blade directive. The integration surface is one call: Htmlawed::filter($html, $config, $spec). For a TPM the blast radius is still one method, and it sits cleanly behind a port/interface in your domain layer. The relevant question remains not "does it fit Laravel" but "does a thin single-maintainer fork deserve to be the trust boundary for user-generated HTML." v2.2.15 is the first evidence in this assessment that the answer might be "yes, with conditions" rather than "no."
Integration feasibility. Still low effort, high stakes. Installation is a one-line Composer require; wiring is a 30-line service provider binding a HtmlSanitizer interface to an adapter. The real work is policy definition, not code: deciding the allowed-tag/allowed-attribute matrix per content type, encoding htmLawed's spec-string config, and building a regression corpus. Budget one engineer-day for the mechanical integration and one to two weeks of part-time effort for security validation and policy work. What v2.2.15 adds to the estimate: this is not a drop-in version bump. The package jumped from v2.2.5 to v2.2.15 — ten upstream htmLawed releases — in a single PR. Whatever accumulated behavior changes, tightened allowlists, or edge-case fixes landed in 1.2.6 through 1.2.14 arrive as one undifferentiated blob, undocumented in the release note. Add one to two engineer-days to diff the vendored Htmlawed.php, read upstream's changelog, and re-run the payload corpus before you accept this version.
Technical risk. Revised, in descending order of severity:
NOASSERTION. Upstream htmLawed is dual-licensed GPL/LGPL, and the fork's packaging does not declare cleanly. GPL-family obligations on a component that touches every stored comment is a distribution question, not a legal footnote. This remains a genuine go/no-go gate. A routine upstream-sync release is not a licensing event, and should not be read as one.htmLawed() function to avoid collision in a Composer-autoloaded app, and confirm no PHP 8.3 deprecation noise in the newly vendored file — upstream 1.2.15 may have modernized some of it, which would be a positive but unverified.Key questions for the vendor/owner. Why this over symfony/html-sanitizer or ezyang/htmlpurifier, which have more eyes and clearer licensing? What is the exact license of the fork as packaged — and has it changed? New: what is the delta between upstream 1.2.5 and 1.2.15, and which of those ten releases contained security-relevant fixes? New: is the fork now on a stated sync cadence, or was this a one-off driven by an internal Vanilla ticket? Who is the internal owner when a bypass is disclosed? Is there a documented path to consume upstream htmLawed security fixes?
Stack fit. Unchanged. Well-suited to high-volume, low-complexity sanitization: forum comments, support tickets, activity feeds, notification bodies. htmLawed's pitch is speed and zero dependencies, and it is materially cheaper per call than HTMLPurifier for typical input sizes. Poorer fit for rich, schema-heavy editorial content where you want structural DOM manipulation; keep that on a heavier library or a dedicated service.
Migration path. Do not rip out an existing sanitizer. Stand up the adapter behind a HtmlSanitizer contract and register it as an alternate implementation. Run shadow mode for two to four weeks: sanitize every write with both the incumbent and htmLawed, log divergences, and diff outputs on a sample of real stored content. For legacy stored rows, prefer sanitize-on-render over rewriting the database — in-place backfill of historical HTML is irreversible and the failure mode is silent content loss.
Compatibility. Framework-agnostic, so Laravel version support reduces to PHP version support; verify explicitly against your pinned PHP runtime with E_ALL and deprecations promoted to errors. Check ext-mbstring/iconv availability. Test in the queue worker context too. New for v2.2.15: if you are already running v2.2.x in production, treat this as a behavior-change release rather than a patch. Re-run the corpus in shadow mode against the new version for at least one release cycle before promoting it, and diff sanitizer output on a stored-content sample — the ten-release upstream delta is precisely where silent over-stripping hides.
Sequencing. (1) License review — hard gate, still open. (2) Spike: adapter + interface + allowlist policy for one content type. (3) Build the payload corpus (OWASP XSS filter evasion, payloadsallthethings) and assert both no bypass and no over-stripping. (4) Shadow mode with divergence logging. (5) Cut over one content type behind a feature flag. (6) Expand per content type, retiring the incumbent path only after a full release cycle of clean divergence data. Insert between 1 and 2: upstream-delta review of 1.2.5→1.2.15 and a manual diff of the vendored file. This is a prerequisite for greenfield adoption of this version and for any existing deployment considering the bump.
Pinning. Pin the exact version — 2.2.15, not ^2.2. The jump from .5 to .15 is an object lesson in why a caret range on a sanitizer is indefensible: a routine composer update would have silently pulled ten upstream releases with no review signal.
Maintenance. The picture improves but the obligation does not disappear. You now have evidence the fork is alive and its packaged version mirrors upstream's (package 2.2.15 bundles htmLawed 1.2.15) — that mirroring is a useful operational property, since it lets you map a package version to an upstream version at a glance and reason about lag. Keep the quarterly upstream review, keep the emergency-vendoring process, and add one rule: any version-number jump of more than a patch increment triggers corpus re-validation, because the size of the delta is your only proxy for risk. Treat the allowlist config as versioned code under review, not environment configuration.
Support. No commercial support, no SLA. Nuance from this release: the change came from @acharron-hl, a first-time contributor who by naming convention appears to be a Vanilla/Higher Logic employee, working an internal ticket. That is mildly reassuring — the fork is maintained by the vendor that owns the downstream product, so it is unlikely to be abandoned outright. But it also confirms the sync schedule is set by Vanilla's priorities. Internal ownership must still be named before adoption: which team responds when a CVE drops on a Friday. If no team will own it, that is the answer.
Scaling. Unchanged. CPU-bound, pure-PHP string processing. Typical comment-sized input (a few KB) costs low single-digit milliseconds — fine inline for request-path writes at moderate volume. Two caveats: cost grows with input length and markup nesting, so hard-cap the input size (e.g., 256 KB) and reject beyond that; and pathological deeply-nested or unterminated-markup inputs can degrade non-linearly, a DoS vector if the sanitizer runs synchronously on untrusted uploads. For bulk imports or large documents, move sanitization to a queue.
Failure modes. Ranked by how quietly they fail: (a) over-stripping — legitimate markup silently deleted, discovered as user complaints weeks later; this release widens the exposure, since ten upstream versions of behavior change land at once; (b) bypass — new mutation XSS vector, discovered by an attacker; (c) config drift — a spec-string edit widens the allowlist without a code-review signal; (d) global function collision or PHP deprecation fatals on runtime upgrade; (e) abandonment — materially less likely after this release, but the failure mode becomes "abandonment after you have stopped watching," which is worse. Each needs a distinct detector; only (d) is loud.
Ramp-up. Mechanically trivial: a competent Laravel engineer wires this in a day and understands htmLawed's spec syntax within a week. The genuine ramp-up cost is on the reviewer side — someone must be able to read a sanitizer allowlist and judge whether it is safe, and read an upstream changelog and judge whether a behavior change matters. That skill is scarcer than the integration work suggests. Plan for one designated reviewer and a written sanitization policy doc before the first content type ships.
How can I help you explore Laravel packages today?