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

symfony/html-sanitizer

Symfony's HtmlSanitizer component offers an object-oriented API to clean untrusted HTML for safe DOM insertion. Configure allowed elements, attributes, link schemes, and URL forcing to strip scripts and dangerous markup.

View on GitHub
Deep Wiki
Context7

Getting Started

First Steps

  1. Installation: Add the package via Composer:

    composer require symfony/html-sanitizer
    

    For Laravel, ensure compatibility with your PHP version (PHP 8.1+ recommended). If you use sanitizeFor() with table-related, image, or plaintext contexts, or you depend on HTML comments being dropped, require ^8.1.8 – earlier 8.1.x releases handle those cases incorrectly.

  2. Basic Setup: Create a sanitizer instance with a minimal config:

    use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
    use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
    
    $config = (new HtmlSanitizerConfig())
        ->allowSafeElements(); // Start with a safe baseline
    
    $sanitizer = new HtmlSanitizer($config);
    
  3. First Use Case: Sanitize user-generated HTML in a comment system:

    $userInput = '<p>Hello <b>World</b>! <script>alert("XSS")</script></p>';
    $cleanHtml = $sanitizer->sanitize($userInput);
    // Output: <p>Hello <b>World</b>!</p>
    

Key Entry Points

  • sanitize(): Core method for sanitizing HTML strings (body context).
  • sanitizeFor(): Context-aware sanitization (head, textarea, plaintext, table-related contexts, image, …). Context coverage was expanded/fixed in v8.1.8.
  • HtmlSanitizerConfig: Builder for rules (elements, attributes, URL schemes/hosts, forced attributes).
  • AttributeSanitizerInterface: Hook for custom per-attribute value sanitization.

Implementation Patterns

Common Workflows

1. Dynamic Content Sanitization

  • Use Case: User-generated content (comments, rich-text editors).
  • Pattern:
    $config = (new HtmlSanitizerConfig())
        ->allowSafeElements()
        ->allowElement('a', ['href', 'title'])
        ->allowElement('img', ['src', 'alt'])
        ->allowAttribute('class', '*')
        ->forceHttpsUrls();
    
    $sanitizer = new HtmlSanitizer($config);
    
    // In a controller:
    $cleanHtml = $sanitizer->sanitize(request()->input('content'));
    return view('post.show', ['content' => $cleanHtml]);
    

2. Context-Specific Sanitization

  • Use Case: Sanitizing HTML fragments that are not full body content.
  • Pattern:
    // For meta/link tags in <head>
    $metaContent = $sanitizer->sanitizeFor('head', $userInput);
    
    // For textarea content (HTML is escaped, not preserved)
    $textareaContent = $sanitizer->sanitizeFor('textarea', $userInput);
    
    // For plain text output (tags stripped, entities escaped) – fixed in 8.1.8
    $plain = $sanitizer->sanitizeFor('plaintext', $userInput);
    
    // For table fragments (td/tr/etc.) – context handling fixed in 8.1.8
    $cell = $sanitizer->sanitizeFor('td', $userInput);
    
    // For SVG <image> context – supported since 8.1.8
    $svgImage = $sanitizer->sanitizeFor('image', $userInput);
    
  • Tip: Any content that is invalid inside the given context is dropped, so a table row sanitized as 'td' loses sibling cells. Pick the context that matches the element the HTML will actually be injected into.

3. URL Sanitization

  • Use Case: Restricting links/media to trusted domains.
  • Pattern:
    $config = (new HtmlSanitizerConfig())
        ->allowSafeElements()
        ->allowLinkSchemes(['https', 'mailto'])
        ->allowLinkHosts(['trusted.com', '*.example.org'])
        ->allowRelativeLinks();
    
    $sanitizer = new HtmlSanitizer($config);
    

4. Attribute-Level Control

  • Use Case: Allowing specific attributes (e.g., data-*) while blocking others.
  • Pattern:
    $config = (new HtmlSanitizerConfig())
        ->allowSafeElements()
        ->allowAttribute('data-custom', '*') // Allow on all elements
        ->dropAttribute('onclick', '*')      // Block globally
        ->forceAttribute('a', 'rel', 'noopener noreferrer');
    

5. Reusable Configurations

  • Use Case: Sharing sanitizer rules across services.
  • Pattern: Keep the configuration file as plain arrays (Laravel serializes/caches config; objects break config:cache), and build the HtmlSanitizerConfig in a service provider or factory:
    // config/sanitizer.php
    return [
        'default' => [
            'elements' => ['a' => ['href', 'title'], 'p', 'br', 'strong', 'em'],
            'force_https' => true,
        ],
        'rich_text' => [
            'elements' => ['a' => ['href', 'title'], 'img' => ['src', 'alt'], 'p', 'br', 'ul', 'ol', 'li'],
        ],
    ];
    

Integration Tips

Laravel-Specific Patterns

  1. Service Provider Binding:

    // app/Providers/AppServiceProvider.php
    public function register()
    {
        $this->app->singleton(HtmlSanitizer::class, function ($app) {
            $config = (new HtmlSanitizerConfig())
                ->allowSafeElements()
                ->allowElement('a', ['href', 'title']);
            return new HtmlSanitizer($config);
        });
    
        // Optional: named sanitizers for different contexts
        $this->app->bind('sanitizer.rich_text', fn () => new HtmlSanitizer(
            (new HtmlSanitizerConfig())
                ->allowStaticElements()
                ->allowElement('a', ['href', 'title'])
                ->allowElement('img', ['src', 'alt'])
        ));
    }
    
  2. Form Request Validation + Sanitization:

    // app/Http/Requests/SanitizeContentRequest.php
    public function validated()
    {
        $data = parent::validated();
        $data['content'] = app(HtmlSanitizer::class)->sanitize($data['content']);
        return $data;
    }
    
  3. Blade Directive:

    // app/Providers/BladeServiceProvider.php
    Blade::directive('sanitize', function ($expression) {
        return "<?php echo app(\\Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer::class)->sanitize({$expression}); ?>";
    });
    
    // In Blade:
    @sanitize($userInput)
    
  4. Model Accessor / Cast for Stored HTML:

    public function getContentAttribute($value): string
    {
        return app(HtmlSanitizer::class)->sanitize($value ?? '');
    }
    

    Sanitizing on output (rather than only on input) is a cheap safety net for legacy rows written before the sanitizer existed.

Performance Considerations

  • Reuse Instances: Register the sanitizer as a singleton and reuse it per request; constructing configs is the expensive part, not the sanitize call.
  • Pre-build Configs: Build HtmlSanitizerConfig once (service provider) instead of per call.
  • Avoid Over-Sanitization: Only allow elements/attributes you explicitly need; allowSafeElements() alone on rich text will strip editor output like ul/ol nesting or class attributes.

Gotchas and Tips

Pitfalls

  1. Attribute Sanitizer Caveats:

    • allowAttribute('*', '*') is not a wildcard for all attributes. Use allowSafeAttributes() or explicitly list attributes.
    • forceAttribute() replaces all values of the attribute, not just adds to them.
  2. URL Handling Quirks:

    • allowRelativeLinks() does not imply allowRelativeMedias(). Configure separately.
    • Hosts with wildcards (e.g., *.example.org) must be exact matches for subdomains.
  3. Context Misuse:

    • sanitizeFor('head', ...) drops <body>-only tags (e.g., <div>), while sanitizeFor('div', ...) is not a valid context – unknown contexts fall back to body-style handling.
    • sanitizeFor('textarea', ...) escapes HTML rather than sanitizing it. Use sanitize() when you need to keep markup.
    • sanitizeFor('plaintext', ...) escapes and strips tags – correct for <plaintext>/pre-formatted output.
    • Table contexts (table, caption, colgroup, col, thead, tbody, tfoot, tr, td, th) and the image context were broken before 8.1.8. Content outside the context's permitted children is silently dropped, so sanitize the smallest fragment you will actually inject.
  4. HTML Comments:

    • As of 8.1.8, comments are no longer rendered as elements when the config's default action is Allow. Do not rely on HTML comments (e.g., <!--more-->, <!-- wp:paragraph -->) surviving sanitization – block-editor markers and WordPress-style excerpt separators will be lost. Strip/handle markers before sanitizing, or store them separately.
  5. Nested Elements:

    • Blocking a parent element (e.g., blockElement('section')) retains children. Use dropElement() to remove them entirely.
  6. Default Action Allow Is Dangerous:

    • allowSafeElements() is a baseline, not "allow everything". Explicitly calling allowElement() per tag is safest; a config that defaults to Allow combined with user content is an XSS risk even with the 8.1.8 comment fix.
  7. PHP 8.4+ Native Parser:

    • On PHP 8.4+, the package uses the native HTML5 parser. Test edge cases (e.g., malformed HTML) if downgrading.

Debugging Tips

  1. Diff Input vs. Output: There is no built-in debug/logging switch. Bisect by sanitizing with progressively larger configs and diffing the result.
    $before = $input;
    $after  = $sanitizer->sanitize($input);
    if ($before !== $after) { logger()->debug('Sanitizer changed content', compact('before', 'after')); }
    
  2. Isolate the Context: If output is unexpectedly empty, check whether it was dropped by the context rules rather than the element rules – try sanitize() and sanitizeFor() side by side.
  3. Wrap for Observability: Decorate the sanitizer in your own class to log every call and result; this is easier than instrumenting the Symfony component.
    class LoggingSanitizer
    {
        public function __construct(private HtmlSanitizer $inner) {}
    
        public function sanitize(string $html): string
        {
            $clean = $this->inner->sanitize($html);
            Log::debug('sanitized', ['in' => $html, 'out' => $clean]);
            return $clean;
        }
    }
    
  4. Test Edge Cases:
    • Malformed HTML: Use <div><p>test</div> to test parser resilience.
    • Unicode/URLs: Test BiDi marks (e.g., &#x202E;) and percent-encoded spaces.
    • Comments: Assert <!-- comment --> is removed – a regression here is exactly what 8.1.8 fixed.
    • Tables/Image/Plaintext: Add data-provider tests over each sanitizeFor() context you use, and pin the minimum version to 8.1.8.

Extension Points

  1. Custom Attribute Sanitizers:

    use Symfony\Component\HtmlSanitizer\AttributeSanitizerInterface;
    
    class CustomAttributeSanitizer implements AttributeSanitizerInterface
    {
        public function sanitize(string $name, string $value, string $element): string
        {
            if ($name === 'data-custom') {
                return preg_replace('/[^a-z0-9_-]/i', '', $value);
            }
            return $value;
        }
    }
    
    $config->withAttributeSanitizer(new CustomAttributeSanitizer());
    
  2. Override Default Rules:

    • Extend HtmlSanitizerConfig for reusable rule sets:
      class AppHtmlSanitizerConfig extends HtmlSanitizerConfig
      {
          public function __construct()
          {
              parent::__construct();
              $this->allowSafeElements()
                   ->allowElement('a', ['href', 'title'])
                   ->forceHttpsUrls();
          }
      }
      
  3. Decorate the Sanitizer:

    • Wrap HtmlSanitizer to apply per-tenant/per-user rules by swapping configs, rather than trying to mutate the config mid-sanitization:
      class TenantSanitizer
      {
          public function __construct(
              private HtmlSanitizerConfig $base,
              private HtmlSanitizerConfig $tenant,
          ) {}
      
          public function sanitize(string $html, bool $isTenant = false): string
          {
              return (new HtmlSanitizer($isTenant ? $this->tenant : $this->base))->sanitize($html);
          }
      }
      

Security Notes

  • Always Sanitize: Never trust user input, even if "pre-sanitized" – re-sanitize on output as well as on input.
  • Upgrade for the Fixes: v8.1.8 is a bugfix release (no breaking changes): comments are no longer emitted as elements under Allow, and sanitizeFor() is corrected for table-related, image, and plaintext contexts. If you were relying on the buggy comment behavior or on table/image context sanitization, re-test after upgrading.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel