symfony/html-sanitizer
Symfony HtmlSanitizer provides an object-oriented API to sanitize untrusted HTML before inserting it into the DOM. Configure allowed/blocked tags and attributes, drop or keep children, force attribute values, enforce HTTPS, and restrict link schemes/hosts to prevent XSS and unsafe behavior.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require symfony/html-sanitizer:^8.1
No additional configuration is required beyond autoloading.
First Use Case: Sanitize user-generated HTML (e.g., from a WYSIWYG editor or comment field) before rendering it in a Laravel Blade view:
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
$sanitizer = new HtmlSanitizer();
$cleanHtml = $sanitizer->sanitize($userInputHtml);
return view('post.show', ['content' => $cleanHtml]);
Where to Look First:
action, formaction, poster, and cite attributes).Basic Sanitization:
$sanitizer = new HtmlSanitizer();
$safeHtml = $sanitizer->sanitize($rawHtml);
action, formaction) to mitigate XSS via malicious URLs.Config-Driven Sanitization with Security Hardening:
$config = (new HtmlSanitizerConfig())
->allowSafeElements()
->allowElement('img', ['src' => ['allowLinkHosts' => ['cdn.example.com']]])
->allowElement('a', ['href' => ['allowLinkHosts' => ['trusted.example.com']]])
->rejectBidirectionalChars(); // New in v8.1: Blocks BiDi override characters
allowLinkHosts/allowMediaHosts to restrict URLs in attributes like href, src, action, etc. The new release fixes bypasses in these rules.Dynamic Configuration for Admin/User Roles:
$config = Auth::user()->isAdmin()
? (new HtmlSanitizerConfig())->allowStaticElements()
: (new HtmlSanitizerConfig())
->allowSafeElements()
->rejectBidirectionalChars()
->allowElement('a', ['href' => ['allowLinkHosts' => ['trusted.example.com']]]);
Laravel Service Provider with Updated Security: Bind the sanitizer to the container with a pre-configured secure config:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(HtmlSanitizer::class, function ($app) {
$config = (new HtmlSanitizerConfig())
->allowSafeElements()
->rejectBidirectionalChars()
->allowElement('a', ['href' => ['allowLinkHosts' => ['trusted.example.com']]])
->allowElement('img', ['src' => ['allowLinkHosts' => ['cdn.example.com']]]);
return new HtmlSanitizer($config);
});
}
Form Request Validation with URL Sanitization: Combine with Laravel’s validation to sanitize after validation, leveraging new URL sanitization:
public function rules()
{
return ['content' => 'required|string'];
}
protected function sanitize($attribute, $value, $fail)
{
$sanitizer = app(HtmlSanitizer::class);
$this->{$attribute} = $sanitizer->sanitize($value);
}
Blade Directives with Security: Create a custom Blade directive for inline sanitization, ensuring URL safety:
// app/Providers/BladeServiceProvider.php
Blade::directive('sanitize', function ($expression) {
return "<?php echo app(\Symfony\Component\HtmlSanitizer\HtmlSanitizer::class)->sanitize({$expression}); ?>";
});
Usage:
{!! sanitize($userHtml) !!}
action or formaction.allowLinkHosts to restrict external URLs.$sanitizer = new HtmlSanitizer((new HtmlSanitizerConfig())
->allowElement('a', ['href' => ['allowLinkHosts' => ['trusted.example.com']]]));
$this->assertEquals(
'<a href="https://trusted.example.com">Safe</a>',
$sanitizer->sanitize('<a href="javascript:alert(1)">Click</a>')
);
URL-Based Attacks:
action, formaction, poster, and cite attributes by default. However, always explicitly whitelist allowed hosts using allowLinkHosts/allowMediaHosts to avoid residual risks.<form action="javascript:alert(1)"> <!-- Previously allowed; now blocked -->
allowElement('form', ['action' => ['allowLinkHosts' => ['trusted.example.com']]]).BiDi (Bidirectional Text) Overrides:
‮) by default to prevent UI redressing attacks.Attribute Wildcard (*):
allowElement('span', '*') still permits all attributes, including dangerous ones like onclick. Prefer explicit whitelists:
->allowElement('span', ['class', 'style'])
Nested Elements:
blockElement('div')) retains its children. Use dropElement() to remove the entire subtree:
->dropElement('div'); // Deletes <div> and all descendants
Performance:
HtmlSanitizer instances is expensive. Reuse a singleton or configure once per request lifecycle.Laravel Caching:
HtmlSanitizer instance across requests, ensure thread safety (Symfony components are stateless, so this is rarely an issue).Inspect Sanitized Output: Compare input/output to identify unintended drops/retentions, especially for URLs:
$raw = '<form action="javascript:alert(1)">Submit</form>';
$clean = $sanitizer->sanitize($raw);
dd($raw, $clean); // Should show the form action is stripped
Log Configuration: Temporarily log the config to verify rules, including URL hosts:
$config = (new HtmlSanitizerConfig())
->allowElement('a', ['href' => ['allowLinkHosts' => ['trusted.example.com']]]);
\Log::debug('Allowed hosts:', $config->getAllowedHosts('href'));
Test Edge Cases:
href, src, action, formaction, poster, cite):
$sanitizer->sanitize('<a href="data:text/html,<script>alert(1)</script>">Click</a>');
$sanitizer->sanitize('<div>‮Text</div>'); // Should be stripped
$sanitizer->sanitize('<a href="https://example.com/path with space">Link</a>');
How can I help you explore Laravel packages today?