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

Html Sanitizer Bundle Laravel Package

tgalopin/html-sanitizer-bundle

Symfony bundle integrating the html-sanitizer library so you can define and configure HTML sanitizers as services, clean untrusted markup safely, and use customizable sanitizer profiles throughout your application.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle (tgalopin/html-sanitizer-bundle) that wires the tgalopin/html-sanitizer library into a Symfony container. If you're coming from Laravel, the first thing to understand is that you probably don't install the bundle itself — Laravel has no bundle auto-discovery, no config/packages/*.yaml merge, and no SanitizerInterface autowiring. In a Laravel app you'll get the same behavior by installing the underlying library directly:

composer require tgalopin/html-sanitizer
# or, for the maintained successor:
composer require symfony/html-sanitizer

Reach for the bundle only when you're working inside a Symfony application (or a shared package that targets both frameworks). For Laravel, treat this assessment as documentation of the sanitizer's configuration surface and extension model, which the bundle exposes.

Minimal first steps:

  1. Identify your profiles. A "profile" is a named sanitizer configuration (e.g. default, markdown, restricted). In Symfony these live in config/packages/html_sanitizer.yaml; in Laravel you'll define them as an array in config/html_sanitizer.php and build HtmlSanitizer instances in a service provider.
  2. Register the sanitizer as a singleton bound to an interface, then inject it into form requests, API resources, or a Blade directive.
  3. First use case: sanitize user-submitted rich text before persistence or before rendering with {!! !!}. Example in Laravel:
// app/Providers/SanitizerServiceProvider.php
$this->app->singleton(SanitizerInterface::class, function () {
    return (new HtmlSanitizerBuilder())
        ->allowSafeElements()
        ->allowRelativeLinks()
        ->allowLinkSchemes(['https', 'mailto'])
        ->build();
});

Then $clean = app(SanitizerInterface::class)->sanitize($request->input('body'));.

Where to look first: the profile definitions (extensions and allowed tags) are the whole game — everything else is plumbing. Start there, not with the bundle's DI wiring.

Implementation Patterns

1. Sanitize-at-write vs. sanitize-at-read. Store the sanitized output at write time (in a FormRequest::passedValidation() or a model mutator) so you have one canonical, safe representation. Sanitizing at read time is fine too, but it leaks the risk that some other render path bypasses your sanitizer. For user-editable content, the common pattern is: store raw HTML in a body_raw column and sanitized HTML in body, so you can re-sanitize historical content when you tighten the profile.

2. One profile per trust boundary. Keep distinct builders for distinct contexts: a permissive article profile (headings, lists, tables, links), a strict comment profile (inline formatting only), and an email profile (limited tags, no <style>). In the Symfony bundle these are separate YAML keys; in Laravel, separate builder methods on a small factory. Don't try to parameterize a single profile with booleans — it becomes untestable.

3. The details extension (new in 1.4.0). This allows <details> / <summary> through the sanitizer. It's exactly what you want for collapsible FAQ blocks, spoiler tags, or progressive-disclosure UI in user content. Enable it explicitly on the profiles that should accept it — it is not part of allowSafeElements(). The extension also needs the relevant attribute allowances (open on <details>) if you want the open/closed state preserved; verify with a fixture rather than assuming.

# Symfony bundle form
html_sanitizer:
    sanitizers:
        article:
            extensions:
                - details
                - code
                - image

In Laravel, mirror this by registering the extension on the builder (see the library's DetailsExtension).

4. Environment-driven overrides (fixed in 1.4.0). 1.4.0 fixes a bug where the key used for env overrides was wrong, meaning %env(...)% interpolation on sanitizer config didn't work as documented. If you were previously working around this by hard-coding allowed iframe hosts or link schemes per environment, you can now move those to env vars (e.g. HTML_SANITIZER_ALLOWED_IFRAMES). Note that env overrides are string-based — comma-separated lists or JSON need explicit decoding in your config layer.

5. Integration points in a Laravel app.

  • Form requests: sanitize in passedValidation() with $this->merge([...]).
  • API resources: sanitize on output if the same column is consumed by multiple clients with different rendering rules (rare; prefer write-time).
  • Blade: a @sanitize($html) directive is convenient but hides the profile choice — prefer {{ $post->sanitized_body }} accessors so the profile is visible at the call site.
  • Queues: sanitization is CPU-light but not free on large documents; for bulk imports, sanitize inside the job so retries re-run against the current profile.

6. Testing. Build a fixture table of input => expected HTML and assert on the sanitized string (or better, on a DOM-parsed comparison, since attribute order and whitespace are not stable). Include adversarial cases: <script> in attributes, javascript: hrefs, SVG payloads, <details open ontoggle=...>. When you bump the bundle/library version, re-run the fixtures — extension behavior can shift between minor releases.

Gotchas and Tips

  • This is a Symfony bundle, not a Laravel package. Composer will install it, but nothing registers it. There's no service provider, no publishable config, no facade. If you add it to a Laravel composer.json you're carrying dead weight. Use the underlying sanitizer library instead.
  • Upstream maintenance. The tgalopin/html-sanitizer library was superseded by symfony/html-sanitizer; the bundle's 1.4.0 release (Symfony 6 support) keeps it installable but doesn't change the long-term direction. New Laravel work should target symfony/html-sanitizer, which carries the same extension architecture. Plan the rename before you have a dozen builders to migrate.
  • details is opt-in and attribute-gated. Enabling the extension alone may strip open, or may allow <details> without <summary> if your element allowlist is narrower. Test the full open/collapse round trip, not just tag survival.
  • Env overrides are strings. The 1.4.0 fix makes %env()% work for sanitizer keys, but arrays (allowed schemes, iframe hosts) won't decode automatically in every Symfony version. If you rely on this, add a config test that asserts the resolved type, not just that it's non-empty.
  • Missing return type deprecation (fixed in 1.4.0). If you've subclassed any bundle classes or implemented interfaces to extend the sanitizer, add the return types now. The fix removes a PHP deprecation notice but also means child classes lacking return types will warn — cheap to fix, easy to miss in CI if deprecations are silenced.
  • Sanitizing is not validating. The sanitizer returns clean-but-mangled HTML; it won't tell you the user's input was malicious. If you need to reject rather than strip, diff input vs. output and act on the delta — but do it in a queued audit, not in the request path.
  • Beware allowSafeElements() as a mental model. "Safe" is defined by the library's allowlist, which is broad. For user content, prefer explicit allowElements([...]) so a library update can't quietly widen what you accept. This is the single highest-leverage defensive habit with this package.
  • Caching. Building profiles is cheap, but in Symfony the bundle compiles them into the container — don't rebuild sanitizers per request in Laravel either; bind them as singletons. Config changes require a container/config cache clear.
  • Extension ordering. Some extensions interact (e.g. code and image both touch <pre>/<img> behavior). If the output looks wrong, temporarily disable extensions one at a time rather than editing the allowlist — the allowlist is often not the culprit.
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/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
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