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.
action, formaction, poster, cite), addressing 3 CVEs related to:
javascript: or data: schemes).allowLinkHosts/allowMediaHosts (e.g., <area> tag misclassification).action, formaction, and poster attributes in HTML forms (e.g., Form::open(['action' => $sanitizedUrl])).action in <form> tags) and media embedding (e.g., poster in <video>).src/poster URLs.action, formaction, poster, or cite attributes will fail unless pre-sanitized.allowLinkHosts/allowMediaHosts now enforce strict URL parsing (e.g., http://example.com vs. //example.com).Url::from() or Str::of()->url() to pre-sanitize URLs before passing to the sanitizer.HtmlSanitizerConfig to explicitly allow trusted domains:
$config = new HtmlSanitizerConfig();
$config->allowLinkHosts(['https://trusted.cdn.com']);
$config->allowMediaHosts(['https://media.example.com']);
symfony/var-dumper).| Risk Area | Mitigation Strategy |
|---|---|
| URL Sanitization Failures | Validate URLs before HTML sanitization using Laravel’s Illuminate\Support\Str::isValidUrl(). |
| Host Whitelisting Errors | Use Laravel’s config() to centralize allowed hosts (e.g., config('sanitizer.allowed_hosts')). |
| BiDi/Encoding Attacks | Add a pre-sanitization filter to strip suspicious characters (e.g., \p{M} Unicode marks). |
| Legacy Forms | Wrap action/formaction in a custom Blade directive to auto-sanitize: |
Blade::directive('secureForm', fn($url) => "<?= app(\\Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer::class)->sanitizeUrl($url) ?>");
``` |
| **Beta Stability** | Test in a **staging environment** with Laravel’s `phpunit` and **OWASP XSS test suites**. |
### **Key Questions**
1. **URL Handling**:
- Are there **dynamic URLs** (e.g., user-uploaded form actions) that require **runtime validation**?
- Should `allowLinkHosts` be **database-driven** (e.g., admin-configurable)?
2. **Legacy Impact**:
- Which **existing forms/media embeds** use unsanitized URLs in `action`/`poster`?
- Can **backward compatibility** be maintained via a **deprecation layer** (e.g., `Sanitizer::legacyMode()`)?
3. **Testing**:
- Should **fuzz testing** be added to Laravel’s CI pipeline for edge-case URLs?
4. **Compliance**:
- Does this release align with **PCI DSS** or **GDPR** requirements for form data handling?
5. **Fallbacks**:
- Define behavior for **malformed URLs** (e.g., `javascript:alert(1)` → strip vs. reject).
---
## Integration Approach
### **Stack Fit**
- **Laravel Core Integration**:
- **Service Provider Update**:
```php
$this->app->singleton(HtmlSanitizer::class, fn() => new HtmlSanitizer(
(new HtmlSanitizerConfig())
->allowSafeElements()
->allowLinkHosts(config('sanitizer.allowed_hosts'))
->rejectBidirectionalChars()
));
```
- **URL Helper**: Extend Laravel’s `Str` facade to include sanitization:
```php
Str::macro('sanitizeUrl', fn($url) => app(HtmlSanitizer::class)->sanitizeUrl($url));
```
- **Form Requests**:
- **Auto-Sanitize Actions**:
```php
protected function prepareForValidation(): void {
$this->merge([
'action' => Str::sanitizeUrl($this->action),
'poster' => Str::sanitizeUrl($this->poster),
]);
}
```
- **Eloquent**:
- **Attribute Casting**:
```php
protected $casts = [
'form_action' => 'sanitized_url',
'video_poster' => 'sanitized_url',
];
```
- **Blade**:
- **Secure Form Directive**:
```php
Blade::directive('secureAction', fn($url) => "<?= Str::sanitizeUrl($url) ?>");
```
Usage:
```html
<form action="{{ secureAction($form->action) }}">
```
### **Migration Path**
1. **Phase 1: URL Pre-Sanitization**
- **Step 1**: Add `Str::sanitizeUrl()` to all dynamic `action`/`poster` attributes.
- **Step 2**: Update `HtmlSanitizerConfig` to enforce `allowLinkHosts`.
- **Step 3**: Test with **OWASP’s XSS payloads** targeting form actions.
2. **Phase 2: Host Whitelisting**
- **Step 1**: Migrate `allowLinkHosts` to Laravel `config()`.
- **Step 2**: Backfill existing URLs to validate against whitelist.
3. **Phase 3: BiDi Protection**
- **Step 1**: Add a **pre-filter** to strip `\p{M}` characters from user input.
- **Step 2**: Update rich-text editors (e.g., CKEditor) to reject BiDi characters.
### **Compatibility**
- **Laravel Ecosystem**:
- **Livewire**: Sanitize `wire:model` URLs in forms.
- **Filament/Spatie**: Update form builders to auto-sanitize `action` fields.
- **Nova**: Custom detail fields for secure URL previews.
- **Third-Party Packages**:
- **Laravel Excel**: Sanitize URLs in exported form data.
- **Spatie Media Library**: Validate `poster`/`src` URLs for video/audio uploads.
- **Legacy Systems**:
- **Deprecation Layer**: Wrap `HtmlSanitizer` in a **compatibility class** to log deprecated usage:
```php
class LegacySanitizer {
public static function sanitize($html) {
if (str_contains($html, 'javascript:')) {
Log::warning('Deprecated URL detected', ['html' => $html]);
}
return app(HtmlSanitizer::class)->sanitize($html);
}
}
```
### **Sequencing**
| Step | Priority | Dependencies | Output |
|--------------------|----------|---------------------------------------|---------------------------------|
| 1. **Update Config** | Critical | Laravel `config/` files | `allowLinkHosts` whitelist |
| 2. **URL Pre-Sanitization** | High | `Str::sanitizeUrl()` helper | Safe form actions |
| 3. **Form Requests** | High | Validation rules | Auto-sanitized `action` fields |
| 4. **Eloquent Casting** | Medium | Database schema | Sanitized URL attributes |
| 5. **Blade Directives** | Medium | Template updates | Secure form rendering |
| 6. **BiDi Filter** | High | User input sources | Stripped malicious characters |
| 7. **Backfill URLs** | Low | Legacy data volume | Validated historical URLs |
| 8.
How can I help you explore Laravel packages today?