, * ce qui casserait les templates. * * Défense en profondeur : les aperçus sont en plus affichés dans des iframes « sandbox ». */ final class TemplateSanitizer { /** Balises retirées avec leur contenu. */ private const BLOCK_TAGS = ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'noscript', 'template', 'svg', 'math']; /** Balises orphelines retirées. */ private const VOID_TAGS = ['base', 'meta', 'link', 'input', 'button', 'select', 'textarea', 'frame', 'frameset']; public function clean(string $html): string { foreach (self::BLOCK_TAGS as $tag) { $html = preg_replace('#<'.$tag.'\b[^>]*>.*?#is', '', $html); $html = preg_replace('#<'.$tag.'\b[^>]*/?>#i', '', $html); // balise ouvrante sans fermeture } foreach (self::VOID_TAGS as $tag) { $html = preg_replace('#]*>#i', '', $html); } // Gestionnaires d'événements : onclick=, onerror=… $html = preg_replace('#\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)#i', '', $html); // URL dangereuses dans href / src / action / style url() $html = preg_replace_callback( '#\s(href|src|action|formaction|xlink:href|background)\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))#i', function (array $m) { $value = html_entity_decode($m[3] !== '' ? $m[3] : ($m[4] ?? $m[5] ?? ''), ENT_QUOTES | ENT_HTML5); $normalized = strtolower(preg_replace('/[\s\x00-\x1f]+/', '', $value)); return preg_match('#^(javascript|vbscript|data):#', $normalized) && ! str_starts_with($normalized, 'data:image/') ? '' : $m[0]; }, $html, ); $html = preg_replace('#expression\s*\(|javascript\s*:#i', '', $html); return trim($html); } }