ldc-ovoteam/app/Http/Middlewares/HtmlSanitizer.php
Jérémy Munsch 108e26f9c0
feat(sanitize): add HTML sanitization utility functions
- Introduced DOMPurify for sanitizing HTML inputs.
- Added `sanitizeRichHtml` and `toSanitizedHtml` functions for safe HTML rendering.
- Updated various components to use the new sanitization methods to prevent XSS attacks.
2026-09-21 11:47:38 +02:00

96 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Middlewares;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
use Mews\Purifier\Purifier;
use Symfony\Component\HttpFoundation\Response;
class HtmlSanitizer
{
public function __construct(
private readonly Purifier $purifierService
) {
}
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request):(\Illuminate\Http\Response) $next
* @param string ...$params List of input names that should be sanitized with rich sanitization (allowing some HTML)
*/
public function handle(Request $request, \Closure $next, string ...$params): Response
{
$inputs = $request->all();
\array_walk($inputs, function (mixed &$input, int | string $name) use ($params): void {
$this->cleanInput($input, (string) $name, null, $params);
});
$request->merge($inputs);
return $next($request);
}
/**
* @param string[] $params
*/
private function cleanInput(mixed &$input, string $name, ?string $rootName, array $params): void
{
if (is_array($input)) {
array_walk_recursive($input, function (mixed &$nestedInput, int | string $nestedName) use ($name, $params): void {
$this->cleanInput($nestedInput, (string) $nestedName, $name, $params);
});
}
if (! is_string($input)) {
return;
}
if ($this->shouldApplyRichSanitization($name, $rootName, $params)) {
$this->sanitizeRichInput($input);
return;
}
$input = strip_tags($input);
}
/**
* @param string[] $params
*/
private function shouldApplyRichSanitization(string $name, ?string $rootName, array $params): bool
{
return in_array($name, $params) || in_array($rootName ?? $name, $params);
}
private function sanitizeRichInput(string &$input): void
{
$isJson = \json_decode($input, true);
if ($isJson && is_array($isJson)) {
\array_walk_recursive($isJson, function (string | array &$item): void {
if (\is_string($item)) {
$item = $this->purifyString($item);
}
});
$input = json_encode($isJson, \JSON_THROW_ON_ERROR) ?: throw new \UnexpectedValueException('Failed to encode JSON after sanitization.');
return;
}
$input = $this->purifyString($input);
}
private function purifyString(string $input): string
{
$cleaned = $this->purifierService->clean($input);
if (! is_string($cleaned)) {
throw new \UnexpectedValueException('Purifier should return a string when input is a string.');
}
return ToolboxHelper::stripCkEditorSchemesOnMediaInputs($cleaned);
}
}