symfony/html-sanitizer
Symfony HtmlSanitizer provides an OO API to clean untrusted HTML for safe DOM insertion. Configure allowed/blocked elements and attributes, drop or keep children, force attributes, enforce HTTPS, and restrict link schemes/hosts to prevent XSS and unsafe behavior.
## Getting Started
### First Steps
1. **Installation**: Add the package via Composer:
```bash
composer require symfony/html-sanitizer
For Laravel, ensure compatibility with your PHP version (PHP 8.1+ recommended).
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);
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>
sanitize(): Core method for sanitizing HTML strings.sanitizeFor(): Context-aware sanitization (e.g., head, textarea).HtmlSanitizerConfig: Builder for rules (elements, attributes, URLs).$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]);
<head> vs. <body>).// For meta tags in <head>
$metaContent = $sanitizer->sanitizeFor('head', $userInput);
// For textarea content (escape HTML)
$textareaContent = $sanitizer->sanitizeFor('textarea', $userInput);
$config = (new HtmlSanitizerConfig())
->allowSafeElements()
->allowLinkSchemes(['https', 'mailto'])
->allowLinkHosts(['trusted.com', '*.example.org'])
->allowRelativeLinks();
$sanitizer = new HtmlSanitizer($config);
data-*) while blocking others.$config = (new HtmlSanitizerConfig())
->allowSafeElements()
->allowAttribute('data-custom', '*') // Allow on all elements
->dropAttribute('onclick', '*') // Block globally
->forceAttribute('a', 'rel', 'noopener noreferrer');
// config/sanitizer.php
return [
'default' => (new HtmlSanitizerConfig())
->allowSafeElements()
->allowElement('div', ['class'])
->forceHttpsUrls(),
'rich_text' => (new HtmlSanitizerConfig())
->allowStaticElements()
->allowElement('a', ['href', 'title'])
->allowElement('img', ['src', 'alt']),
];
// In a service:
$sanitizer = new HtmlSanitizer(config('sanitizer.default'));
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);
});
}
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;
}
Blade Directives:
// app/Providers/BladeServiceProvider.php
Blade::directive('sanitize', function ($expression) {
return "<?php echo app(\\Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer::class)->sanitize({$expression}); ?>";
});
// In Blade:
@sanitize($userInput)
HtmlSanitizer once per context (e.g., per HTTP request).HtmlSanitizerConfig.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.URL Handling Quirks:
allowRelativeLinks() does not imply allowRelativeMedias(). Configure separately.*.example.org) must be exact matches for subdomains.Context Misuse:
sanitizeFor('head', ...) drops <body>-only tags (e.g., <div>), but sanitizeFor('div', ...) treats it as body context.sanitizeFor('textarea', ...) escapes HTML (not sanitizes). Use sanitize() for HTML content.Nested Elements:
blockElement('section')) retains children. Use dropElement() to remove them entirely.PHP 8.4+ Native Parser:
Inspect Sanitized Output:
$sanitizer->sanitize($input, true); // Returns array with warnings/errors
Log Configuration:
$config->debug(true); // Logs dropped elements/attributes
Test Edge Cases:
<div><p>test</div> to test parser resilience.‮) and percent-encoded spaces.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());
Override Default Rules:
HtmlSanitizerConfig for reusable rule sets:
class AppHtmlSanitizerConfig extends HtmlSanitizerConfig
{
public function __construct()
{
parent::__construct();
$this->allowSafeElements()
->allowElement('a', ['href', 'title'])
->forceHttpsUrls();
}
}
Event Listeners:
$sanitizer->addListener('sanitize', function ($event) {
if ($event->getContext() === 'body') {
$event->getConfig()->allowElement('custom-tag');
}
});
How can I help you explore Laravel packages today?