|array|string[]
146 | */
147 | public function inlineListenersPatterns(): array
148 | {
149 | return [$this->inlineListenersPattern, $this->malformedListenersPattern];
150 | }
151 |
152 | public function shouldEscapeInlineListeners(): bool
153 | {
154 | return $this->escapeInlineListeners;
155 | }
156 |
157 | public function allowedMediaHosts(): ?array
158 | {
159 | return $this->allowedMediaHosts;
160 | }
161 |
162 | public function setAllowedElements(?array $allowedElements): CleanerConfig
163 | {
164 | $this->allowedElements = $allowedElements;
165 |
166 | return $this;
167 | }
168 |
169 | public function setDeniedElements(array $deniedElements): CleanerConfig
170 | {
171 | $this->deniedElements = $deniedElements;
172 |
173 | return $this;
174 | }
175 |
176 | public function setBlockedElements(array $blockedElements): CleanerConfig
177 | {
178 | return $this->setDeniedElements($blockedElements);
179 | }
180 |
181 | public function setMediaElements(array $mediaElements): CleanerConfig
182 | {
183 | $this->mediaElements = $mediaElements;
184 |
185 | return $this;
186 | }
187 |
188 | public function setEscapeInlineListeners(bool $escapeInlineListeners): CleanerConfig
189 | {
190 | $this->escapeInlineListeners = $escapeInlineListeners;
191 |
192 | return $this;
193 | }
194 |
195 | public function setAllowedMediaHosts(?array $allowedMediaHosts): CleanerConfig
196 | {
197 | $this->allowedMediaHosts = $allowedMediaHosts;
198 |
199 | return $this;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/Facade/XSSCleaner.php:
--------------------------------------------------------------------------------
1 | except = config('xss-filter.except', []);
23 | }
24 |
25 | /**
26 | * Transform the given value.
27 | *
28 | * @param string $key
29 | * @param mixed $value
30 | *
31 | * @return string|mixed
32 | */
33 | protected function transform($key, $value): mixed
34 | {
35 | if (in_array($key, $this->except, true)) {
36 | return $value;
37 | }
38 |
39 | if (!is_string($value)) {
40 | return $value;
41 | }
42 |
43 | return $this->cleaner->clean($value);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Middleware/FilterXSSLivewire.php:
--------------------------------------------------------------------------------
1 | isLivewireRequest($request)) {
15 | return $next($request);
16 | }
17 |
18 | $this->cleanLivewirePayload($request);
19 |
20 | return $next($request);
21 | }
22 |
23 | protected function cleanLivewirePayload(Request $request): void
24 | {
25 | $components = $request->input('components');
26 |
27 | foreach ($components as $i => &$component) {
28 | if (isset($component['updates'])) {
29 | $component['updates'] = $this->cleanArray($component['updates'], "components.{$i}.updates.");
30 | }
31 |
32 | if (isset($component['calls'])) {
33 | foreach ($component['calls'] as $j => &$call) {
34 | $call['params'] = $this->cleanArray($call['params'], "components.{$i}.calls.{$j}.params.");
35 | }
36 | }
37 | }
38 |
39 | $request->request->set('components', $components);
40 | }
41 |
42 | protected function isLivewireRequest(Request $request): bool
43 | {
44 | return $request->routeIs('*livewire.update');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/XSSFilterServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
16 | __DIR__ . '/../config/xss-filter.php' => config_path('xss-filter.php'),
17 | ], 'config');
18 | }
19 |
20 | public function register(): void
21 | {
22 | $this->mergeConfigFrom(__DIR__ . '/../config/xss-filter.php', 'xss-filter');
23 |
24 | $this->app->scoped(Cleaner::class, static function () {
25 | return new Cleaner(CleanerConfig::fromArray(config('xss-filter')));
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/FilterXSSTest.php:
--------------------------------------------------------------------------------
1 | request = Request::create($url, 'POST', $data);
33 |
34 | return $this->request;
35 | }
36 |
37 | protected function responseFromMiddlewareWithInput(array $input = []): void
38 | {
39 | app(FilterXSS::class)
40 | ->handle($this->request($input), function () {
41 | // nothing to do here
42 | });
43 | }
44 |
45 | #[Test]
46 | public function it_doesnt_change_non_html_inputs(): void
47 | {
48 | $this->responseFromMiddlewareWithInput($input = ['text' => 'Simple text', 'number' => 56]);
49 |
50 | $this->assertEquals($input, $this->request->all());
51 | }
52 |
53 | #[Test]
54 | public function it_escapes_script_tags(): void
55 | {
56 | $this->responseFromMiddlewareWithInput([
57 | 'with_src' => 'Before text after text',
58 | 'multiline' => "Before text \n \n After text",
59 | ]);
60 |
61 | $this->assertEquals([
62 | 'with_src' => 'Before text ' . e('') . ' after text',
63 | 'multiline' => "Before text \n " . e("") . "\n After text",
64 | ], $this->request->all());
65 | }
66 |
67 | #[Test]
68 | public function it_doesnt_change_non_script_html_inputs(): void
69 | {
70 | $this->responseFromMiddlewareWithInput([
71 | 'html_with_script_src' => ' test on some text test test test',
72 | 'html_with_script_multiline' => " \n test on some text test test test",
73 | ]);
74 |
75 | $this->assertEquals([
76 | 'html_with_script_src' => 'link text Before text ' . e('') . ' after text
test on some text test test test',
77 | 'html_with_script_multiline' => "\n
link text \n Before text \n " . e("") . "\n After text
\n test on some text test test test",
78 | ], $this->request->all());
79 | }
80 |
81 | #[Test]
82 | public function it_escapes_embed_elements(): void
83 | {
84 | $this->responseFromMiddlewareWithInput([
85 | 'iframe' => 'Before text after text.
',
86 | 'iframe_multiline' => '\nBefore text\n\n after text.\n
',
87 | 'object' => '',
88 | 'object_multiline' => '\nBefore text\n
\n after text.\n
',
89 | ]);
90 |
91 | $this->assertEquals([
92 | 'iframe' => 'Before text' . e('') . ' after text.
',
93 | 'iframe_multiline' => '\nBefore text\n' . e('') . '\n after text.\n
',
94 | 'object' => 'Before text' . e('
') . ' after text.
',
95 | 'object_multiline' => '\nBefore text\n' . e('
') . '\n after text.\n
',
96 | ], $this->request->all());
97 | }
98 |
99 | #[Test]
100 | public function it_removes_inline_listeners(): void
101 | {
102 | $this->responseFromMiddlewareWithInput([
103 | 'html' => '',
104 | 'html_multiline' => "",
105 | ]);
106 |
107 | $this->assertEquals([
108 | 'html' => '',
109 | 'html_multiline' => "",
110 | ], $this->request->all());
111 | }
112 |
113 | #[Test]
114 | public function it_removes_img_inline_listeners(): void
115 | {
116 | $this->responseFromMiddlewareWithInput([
117 | 'html' => 'test ',
118 | ]);
119 |
120 | $this->assertEquals([
121 | 'html' => 'test ',
122 | ], $this->request->all());
123 | }
124 |
125 | #[Test]
126 | public function it_removes_inline_listeners_with_string_params(): void
127 | {
128 | $this->responseFromMiddlewareWithInput([
129 | 'html' => 'test ',
130 | 'html' => 'test ',
131 | ]);
132 |
133 | $this->assertEquals([
134 | 'html' => 'test ',
135 | 'html' => 'test ',
136 | ], $this->request->all());
137 | }
138 |
139 | #[Test]
140 | public function it_removes_inline_listeners_from_invalid_html(): void
141 | {
142 | $this->responseFromMiddlewareWithInput([
143 | 'html' => '',
144 | 'html_multiline' => "",
145 | ]);
146 |
147 | $this->assertEquals([
148 | 'html' => '',
149 | 'html_multiline' => "",
150 | ], $this->request->all());
151 | }
152 |
153 | #[Test]
154 | public function it_clears_nested_inputs(): void
155 | {
156 | $this->responseFromMiddlewareWithInput([
157 | 'value1' => 'Value 1',
158 | 'value2' => 2,
159 | 'html' => [
160 | 'oneline' => '',
161 | 'multline' => "\n",
162 | ],
163 | 'value3' => [
164 | 'value3_1' => 'Value 3-1',
165 | 'value3_2' => 32,
166 | ],
167 | ]);
168 |
169 | $this->assertEquals([
170 | 'value1' => 'Value 1',
171 | 'value2' => 2,
172 | 'html' => [
173 | 'oneline' => 'link text Before text ' . e('') . ' after text
',
174 | 'multline' => "\n\n
link text \n Before text \n " . e("") . "\n After text
",
175 | ],
176 | 'value3' => [
177 | 'value3_1' => 'Value 3-1',
178 | 'value3_2' => 32,
179 | ],
180 | ], $this->request->all());
181 | }
182 |
183 | #[Test]
184 | public function it_dont_convert_0_to_empty_string(): void
185 | {
186 | $this->responseFromMiddlewareWithInput($input = ['text' => '0']);
187 |
188 | $this->assertEquals($input, $this->request->all());
189 | }
190 |
191 | #[Test]
192 | public function it_removes_inline_javascript_in_href(): void
193 | {
194 | $this->responseFromMiddlewareWithInput([
195 | 'html' => '',
196 | 'html_multiline' => "",
197 | ]);
198 |
199 | $this->assertEquals([
200 | 'html' => '',
201 | 'html_multiline' => "",
202 | ], $this->request->all());
203 | }
204 |
205 | #[Test]
206 | public function it_doest_not_touch_other_attributes(): void
207 | {
208 | $this->responseFromMiddlewareWithInput([
209 | 'html' => 'text
',
210 | 'html_multiline' => "\n\ntext\n \n
",
211 | ]);
212 |
213 | $this->assertEquals([
214 | 'html' => 'text
',
215 | 'html_multiline' => "\n\ntext\n \n
",
216 | ], $this->request->all());
217 | }
218 |
219 | #[Test]
220 | public function it_escapes_inline_event_listeners(): void
221 | {
222 | XSSCleaner::config()->setEscapeInlineListeners(true);
223 |
224 | $this->responseFromMiddlewareWithInput([
225 | 'html' => 'text
',
226 | 'html_multiline' => "\n\ntext\n \n
",
227 | ]);
228 |
229 | $this->assertEquals([
230 | 'html' => 'text
',
231 | 'html_multiline' => "\n\ntext\n \n
",
232 | ], $this->request->all());
233 | }
234 |
235 | #[Test]
236 | public function it_cleans_disallowed_media_hosts(): void
237 | {
238 | XSSCleaner::config()->allowElement('iframe')->allowMediaHosts(['youtube.com']);
239 |
240 | $this->responseFromMiddlewareWithInput([
241 | 'iframe' => 'Before text after text.
',
242 | 'iframe_multiline' => '\nBefore text\n\n after text.\n
',
243 | 'video' => 'Before text after text.
',
244 | 'video_multiline' => '\nBefore text\n\n\n \n after text.\n
',
245 | ]);
246 |
247 | $this->assertEquals([
248 | 'iframe' => 'Before text after text.
',
249 | 'iframe_multiline' => '\nBefore text\n\n after text.\n
',
250 | 'video' => 'Before text after text.
',
251 | 'video_multiline' => '\nBefore text\n\n\n \n after text.\n
',
252 | ], $this->request->all());
253 | }
254 |
255 | #[Test]
256 | public function it_does_not_escape_allowed_media_hosts(): void
257 | {
258 | XSSCleaner::config()->allowElement('iframe')->allowMediaHosts([
259 | 'example.test',
260 | 'https://video.test',
261 | 'youtu.be',
262 | ]);
263 |
264 | $this->responseFromMiddlewareWithInput([
265 | 'iframe' => 'Before text after text.
',
266 | 'iframe_multiline' => '\nBefore text\n\n after text.\n
',
267 | 'video' => 'Before text after text.
',
268 | 'video_multiline' => '\nBefore text\n\n\n \n after text.\n
',
269 | ]);
270 |
271 | $this->assertEquals([
272 | 'iframe' => 'Before text after text.
',
273 | 'iframe_multiline' => '\nBefore text\n\n after text.\n
',
274 | 'video' => 'Before text after text.
',
275 | 'video_multiline' => '\nBefore text\n\n\n \n after text.\n
',
276 | ], $this->request->all());
277 | }
278 | }
279 |
--------------------------------------------------------------------------------