typo3/html-sanitizer
Standalone PHP HTML sanitizer from TYPO3. Define sanitization rules via Behavior, apply with Visitors, and get a ready-to-use Sanitizer via Builders/presets. Control allowed tags, attributes, and values; encode or remove invalid nodes and comments.
Installation
composer require typo3/html-sanitizer
Basic Usage
Import the CommonBuilder for predefined safe HTML rules:
use TYPO3\HtmlSanitizer\Builder\CommonBuilder;
use TYPO3\HtmlSanitizer\Sanitizer;
$builder = new CommonBuilder();
$sanitizer = $builder->build();
$cleanHtml = $sanitizer->sanitize($unsafeHtml);
First Use Case Sanitize user-generated content (e.g., comments, forum posts) to prevent XSS:
$userInput = '<script>alert("XSS")</script><p>Safe content</p>';
$safeOutput = $sanitizer->sanitize($userInput);
// Output: <script>alert("XSS")</script><p>Safe content</p>
CommonBuilder: Predefined safe HTML rules (e.g., <p>, <a>, <img>).Behavior: Customize allowed tags, attributes, and values.Sanitizer: Core class for sanitizing HTML.Define Rules
Use CommonBuilder or create a custom Behavior:
$behavior = (new Behavior())
->withTags(
(new Behavior\Tag('div'))->addAttrs(new Behavior\Attr('class')),
(new Behavior\Tag('a'))->addAttrs(
(new Behavior\Attr('href'))
->addValues(new Behavior\RegExpAttrValue('#^https?://#'))
)
);
Build Sanitizer
$sanitizer = new Sanitizer($behavior, new CommonVisitor($behavior));
Sanitize Input
$cleanHtml = $sanitizer->sanitize($userInput);
Laravel Blade Directives Create a custom Blade directive for reusable sanitization:
Blade::directive('sanitize', function ($expression) {
$sanitizer = resolve(Sanitizer::class);
return "<?php echo {$sanitizer}->sanitize({$expression}); ?>";
});
Usage:
@sanitize($userComment)
Form Request Validation
Sanitize input in FormRequest validation:
public function rules()
{
return [
'bio' => 'required|string',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
$sanitizer = app(Sanitizer::class);
$validator->request->merge([
'bio' => $sanitizer->sanitize($validator->request->bio),
]);
});
}
Service Provider Binding
Bind Sanitizer in AppServiceProvider:
public function register()
{
$this->app->singleton(Sanitizer::class, function ($app) {
$builder = new CommonBuilder();
return $builder->build();
});
}
Dynamic Rule Sets
Use BuilderInterface to create multiple sanitizers for different contexts (e.g., admin vs. public content):
$adminBuilder = new CommonBuilder();
$adminBuilder->allowTags(['script']); // Only for trusted admins
$adminSanitizer = $adminBuilder->build();
Immutable Behavior
Methods like withTags() return a new instance. Always reassign:
$behavior = $behavior->withTags(new Behavior\Tag('span'));
Mandatory Attributes
Forgetting to mark attributes as mandatory (e.g., href for <a>) will silently remove the tag:
(new Behavior\Tag('a'))
->addAttrs(
(new Behavior\Attr('href'))
->withFlags(Behavior\Attr::MANDATORY)
);
Custom Elements
Avoid ALLOW_CUSTOM_ELEMENTS flag. Explicitly list allowed tags (e.g., <my-custom>) to prevent unexpected HTML.
Encoding Flags
ENCODE_INVALID_TAG keeps invalid tags but "disarms" them (e.g., <script> becomes <script>). Use REMOVE_UNEXPECTED_CHILDREN to strip children from disallowed tags.
CDATA and Comments By default, comments and CDATA sections are allowed but encoded. Explicitly disable them:
$behavior->withoutNodes(new Behavior\Comment());
Raw Text Bypass
Avoid ALLOW_INSECURE_RAW_TEXT unless absolutely necessary (security risk). Use custom handlers instead:
$behavior->withNodes(
new Behavior\NodeHandler(
new Behavior\Tag('raw'),
new Behavior\Handler\ClosureHandler(
fn ($node) => $node->textContent // Custom logic
)
)
);
Log Sanitization Steps
Enable debug logging in CommonVisitor:
$visitor = new CommonVisitor($behavior, [
'debug' => true,
'logger' => \Psr\Log\LoggerInterface::class,
]);
Inspect DOM Nodes
Use DOMDocument to debug parsed HTML:
$dom = new DOMDocument();
$dom->loadHTML($userInput, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new DOMXPath($dom);
$nodes = $xpath->query('//script'); // Check for disallowed nodes
Custom Visitors
Extend VisitorInterface to add preprocessing/postprocessing:
class MyVisitor implements VisitorInterface
{
public function visit(NodeInterface $node, DOMNode $domNode): void
{
if ($domNode->nodeName === 'a' && !$domNode->hasAttribute('href')) {
$domNode->parentNode->removeChild($domNode);
}
}
}
Attribute Value Validation
Use RegExpAttrValue or CallbackAttrValue for dynamic validation:
$emailAttr = (new Behavior\Attr('data-email'))
->addValues(new Behavior\CallbackAttrValue(
fn ($value) => filter_var($value, FILTER_VALIDATE_EMAIL) !== false
));
Output Rules Override serialization rules (e.g., for custom tags):
$sanitizer = new Sanitizer($behavior, new CommonVisitor($behavior));
$sanitizer->setSerializer(new CustomSerializer($behavior));
Performance
Reuse Sanitizer instances (they are stateless). Cache builders for common use cases:
$cache = new \Symfony\Component\Cache\Simple\FilesystemCache();
$builder = $cache->get('html_sanitizer_builder', function () {
return new CommonBuilder();
});
Always Sanitize Early
Sanitize input before processing (e.g., in FormRequest or controller) to fail fast.
Test Edge Cases Test with payloads like:
<svg onload=alert(1)>
<script>/*<![CDATA[*/alert(1)//]]>*/
Ensure they are encoded or removed.
Avoid Whitelisting javascript:
Even if allowed, javascript: URIs can bypass sanitization. Use RegExpAttrValue to restrict to http:///https:// only.
Store Sanitizer in Cache Reduce overhead for repeated requests:
$sanitizer = Cache::remember('html_sanitizer', now()->addHours(1), function () {
return (new CommonBuilder())->build();
});
Use in Form Requests Sanitize input during validation:
public function passedValidation()
{
$this->merge([
'content' => app(Sanitizer::class)->sanitize($this->content),
]);
}
Middleware for Global Sanitization Sanitize all user input in middleware:
public function handle($request, Closure $next)
{
$sanitizer = app(Sanitizer::class);
$request->replace(array_map(
fn ($value) => is_string($value) ? $sanitizer->sanitize($value) : $value,
How can I help you explore Laravel packages today?