Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.35% covered (success)
95.35%
41 / 43
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentSanitizer
95.35% covered (success)
95.35%
41 / 43
71.43% covered (warning)
71.43%
5 / 7
9
0.00% covered (danger)
0.00%
0 / 1
 sanitize
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 removeScriptTags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 removeEventHandlers
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 removeDangerousUrls
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
1.00
 removePluginTags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 removeMetaTags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 sanitizeStyleAttributes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3
4namespace App\Utility;
5
6/**
7 * ContentSanitizer Utility
8 *
9 * Provides HTML sanitization to prevent XSS attacks while preserving
10 * safe HTML formatting for CMS content (articles, pages, etc.).
11 *
12 * Security features:
13 * - Removes <script> tags and their content
14 * - Removes event handler attributes (onclick, onerror, onload, etc.)
15 * - Removes javascript:, vbscript:, and data: URLs from href/src attributes
16 * - Preserves safe HTML tags for content formatting
17 */
18class ContentSanitizer
19{
20    /**
21     * Event handler attributes to remove (XSS vectors)
22     *
23     * @var array<string>
24     */
25    private static array $dangerousAttributes = [
26        'onabort', 'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onblur',
27        'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'oncontextmenu',
28        'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend',
29        'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop',
30        'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus',
31        'onhashchange', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
32        'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart',
33        'onmessage', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',
34        'onmouseup', 'onmousewheel', 'onoffline', 'ononline', 'onpagehide',
35        'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpopstate',
36        'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll',
37        'onsearch', 'onseeked', 'onseeking', 'onselect', 'onstalled', 'onstorage',
38        'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onunload',
39        'onvolumechange', 'onwaiting', 'onwheel', 'onanimationend',
40        'onanimationiteration', 'onanimationstart', 'ontransitionend',
41        'onpointerdown', 'onpointerup', 'onpointermove', 'onpointerenter',
42        'onpointerleave', 'onpointerover', 'onpointerout', 'onpointercancel',
43        'ongotpointercapture', 'onlostpointercapture',
44    ];
45
46    /**
47     * Sanitize HTML content to prevent XSS attacks
48     *
49     * @param string|null $html The HTML content to sanitize
50     * @return string The sanitized HTML content
51     */
52    public static function sanitize(?string $html): string
53    {
54        if (empty($html)) {
55            return '';
56        }
57
58        // Remove script tags and their content
59        $html = self::removeScriptTags($html);
60
61        // Remove dangerous event handler attributes
62        $html = self::removeEventHandlers($html);
63
64        // Remove javascript:, vbscript:, data: URLs
65        $html = self::removeDangerousUrls($html);
66
67        // Remove <object>, <embed>, <applet> tags (Flash/plugin vectors)
68        $html = self::removePluginTags($html);
69
70        // Remove <meta>, <link>, <base> tags that could redirect or inject
71        $html = self::removeMetaTags($html);
72
73        // Remove style attributes containing expressions or javascript
74        $html = self::sanitizeStyleAttributes($html);
75
76        return $html;
77    }
78
79    /**
80     * Remove script tags and their content
81     *
82     * @param string $html The HTML content
83     * @return string HTML with script tags removed
84     */
85    private static function removeScriptTags(string $html): string
86    {
87        // Remove <script>...</script> including content
88        $html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html);
89
90        // Remove self-closing script tags
91        $html = preg_replace('/<script\b[^>]*\/>/is', '', $html);
92
93        // Remove orphaned opening script tags
94        $html = preg_replace('/<script\b[^>]*>/is', '', $html);
95
96        return $html ?? '';
97    }
98
99    /**
100     * Remove event handler attributes
101     *
102     * @param string $html The HTML content
103     * @return string HTML with event handlers removed
104     */
105    private static function removeEventHandlers(string $html): string
106    {
107        foreach (self::$dangerousAttributes as $attr) {
108            // Match the attribute with various quote styles and whitespace
109            $pattern = '/\s+' . preg_quote($attr, '/') . '\s*=\s*(["\'])[^"\']*\1/is';
110            $html = preg_replace($pattern, '', $html);
111
112            // Handle unquoted attribute values
113            $pattern = '/\s+' . preg_quote($attr, '/') . '\s*=\s*[^\s>]+/is';
114            $html = preg_replace($pattern, '', $html);
115        }
116
117        return $html ?? '';
118    }
119
120    /**
121     * Remove dangerous URL schemes from href and src attributes
122     *
123     * @param string $html The HTML content
124     * @return string HTML with dangerous URLs removed
125     */
126    private static function removeDangerousUrls(string $html): string
127    {
128        // Remove javascript: URLs from common attributes
129        $pattern = '/\b(href|src|action|formaction|poster|data)\s*=\s*(["\'])?\s*javascript\s*:[^"\'>\s]*/is';
130        $html = preg_replace($pattern, '$1=$2#blocked', $html);
131
132        // Remove vbscript: URLs from common attributes
133        $pattern = '/\b(href|src|action|formaction|poster|data)\s*=\s*(["\'])?\s*vbscript\s*:[^"\'>\s]*/is';
134        $html = preg_replace($pattern, '$1=$2#blocked', $html);
135
136        // Remove data: URLs (except for safe image types)
137        $html = preg_replace_callback(
138            '/\b(src)\s*=\s*(["\'])\s*data:(?!image\/(png|gif|jpeg|webp);base64,)[^"\'>\s]*/is',
139            function ($matches) {
140                return $matches[1] . '=' . $matches[2] . '#blocked';
141            },
142            $html,
143        );
144
145        return $html ?? '';
146    }
147
148    /**
149     * Remove plugin embedding tags
150     *
151     * @param string $html The HTML content
152     * @return string HTML with plugin tags removed
153     */
154    private static function removePluginTags(string $html): string
155    {
156        // Remove <object> tags
157        $html = preg_replace('/<object\b[^>]*>.*?<\/object>/is', '', $html);
158
159        // Remove <embed> tags
160        $html = preg_replace('/<embed\b[^>]*\/?>/is', '', $html);
161
162        // Remove <applet> tags
163        $html = preg_replace('/<applet\b[^>]*>.*?<\/applet>/is', '', $html);
164
165        return $html ?? '';
166    }
167
168    /**
169     * Remove meta, link, and base tags
170     *
171     * @param string $html The HTML content
172     * @return string HTML with meta tags removed
173     */
174    private static function removeMetaTags(string $html): string
175    {
176        $html = preg_replace('/<meta\b[^>]*\/?>/is', '', $html);
177        $html = preg_replace('/<link\b[^>]*\/?>/is', '', $html);
178        $html = preg_replace('/<base\b[^>]*\/?>/is', '', $html);
179
180        return $html ?? '';
181    }
182
183    /**
184     * Sanitize style attributes to remove expression() and javascript
185     *
186     * @param string $html The HTML content
187     * @return string HTML with sanitized style attributes
188     */
189    private static function sanitizeStyleAttributes(string $html): string
190    {
191        // Remove style attributes containing expression()
192        $html = preg_replace('/\bstyle\s*=\s*(["\'])[^"\']*expression\s*\([^"\']*\1/is', '', $html);
193
194        // Remove style attributes containing javascript
195        $html = preg_replace('/\bstyle\s*=\s*(["\'])[^"\']*javascript\s*:[^"\']*\1/is', '', $html);
196
197        // Remove style attributes containing url() with javascript
198        $html = preg_replace('/\bstyle\s*=\s*(["\'])[^"\']*url\s*\(\s*["\']?\s*javascript:[^"\']*\1/is', '', $html);
199
200        return $html ?? '';
201    }
202}