• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / anti-xss / 9348412493

03 Jun 2024 10:20AM UTC coverage: 98.422%. Remained the same
9348412493

Pull #124

github

web-flow
Merge c0c398469 into 25a3cf6a6
Pull Request #124: Update shivammathur/setup-php action to v2.30.5

2245 of 2281 relevant lines covered (98.42%)

145.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.32
/src/voku/helper/AntiXSS.php
1
<?php
2

3
/** @noinspection ReturnTypeCanBeDeclaredInspection */
4

5
declare(strict_types=1);
6

7
namespace voku\helper;
8

9
use const ENT_DISALLOWED;
10
use const ENT_HTML5;
11
use const ENT_QUOTES;
12
use const ENT_SUBSTITUTE;
13
use const HTML_ENTITIES;
14

15
/**
16
 * AntiXSS - ported from "CodeIgniter"
17
 *
18
 * ## 🇷🇺 Русским гражданам
19
 * В Украине сейчас идет война. Силами РФ наносятся удары по гражданской инфраструктуре в [Харькове][1], [Киеве][2], [Чернигове][3], [Сумах][4], [Ирпене][5] и десятках других городов. Гибнут люди - и гражданское население, и военные, в том числе российские призывники, которых бросили воевать. Чтобы лишить собственный народ доступа к информации, правительство РФ запретило называть войну войной, закрыло независимые СМИ и принимает сейчас ряд диктаторских законов. Эти законы призваны заткнуть рот всем, кто против войны. За обычный призыв к миру сейчас можно получить несколько лет тюрьмы.
20
 * Не молчите! Молчание - знак вашего согласия с политикой российского правительства.
21
 *  **Вы можете сделать выбор НЕ МОЛЧАТЬ.**
22
 * ---
23
 * ## 🇺🇸 To people of Russia
24
 * There is a war in Ukraine right now. The forces of the Russian Federation are attacking civilian infrastructure in [Kharkiv][1], [Kyiv][2], [Chernihiv][3], [Sumy][4], [Irpin][5] and dozens of other cities. People are dying – both civilians and military servicemen, including Russian conscripts who were thrown into the fighting. In order to deprive its own people of access to information, the government of the Russian Federation has forbidden calling a war a war, shut down independent media and is passing a number of dictatorial laws. These laws are meant to silence all those who are against war. You can be jailed for multiple years for simply calling for peace.
25
 * Do not be silent! Silence is a sign that you accept the Russian government's policy.
26
 * **You can choose NOT TO BE SILENT.**
27
 * ---
28
 * - [1] https://cloudfront-us-east-2.images.arcpublishing.com/reuters/P7K2MSZDGFMIJPDD7CI2GIROJI.jpg "Kharkiv under attack"
29
 * - [2] https://gdb.voanews.com/01bd0000-0aff-0242-fad0-08d9fc92c5b3_cx0_cy5_cw0_w1023_r1_s.jpg "Kyiv under attack"
30
 * - [3] https://ichef.bbci.co.uk/news/976/cpsprodpb/163DD/production/_123510119_hi074310744.jpg "Chernihiv under attack"
31
 * - [4] https://www.youtube.com/watch?v=8K-bkqKKf2A "Sumy under attack"
32
 * - [5] https://cloudfront-us-east-2.images.arcpublishing.com/reuters/K4MTMLEHTRKGFK3GSKAT4GR3NE.jpg "Irpin under attack"
33
 *
34
 * @copyright   Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
35
 * @copyright   Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
36
 * @copyright   Copyright (c) 2015 - 2020, Lars Moelleken (https://moelleken.org/)
37
 * @license     http://opensource.org/licenses/MIT        MIT License
38
 */
39
final class AntiXSS
40
{
41
    const VOKU_ANTI_XSS_GT = 'voku::anti-xss::gt';
42

43
    const VOKU_ANTI_XSS_LT = 'voku::anti-xss::lt';
44

45
    /**
46
     * @deprecated will be removed in future versions
47
     */
48
    const VOKU_ANTI_XSS_STYLE = 'voku::anti-xss::STYLE';
49

50
    /**
51
     * List of never allowed regex replacements.
52
     *
53
     * @var string[]
54
     */
55
    private $_never_allowed_regex = [];
56

57
    /**
58
     * List of html tags that will not close automatically.
59
     *
60
     * @var string[]
61
     */
62
    private $_do_not_close_html_tags = [];
63

64
    /**
65
     * List of never allowed call statements.
66
     *
67
     * @var string[]
68
     */
69
    private $_never_allowed_js_callback_regex = [
70
        '\(?window\)?\.',
71
        '\(?history\)?\.',
72
        '\(?location\)?\.',
73
        '\(?document\)?\.',
74
        '\(?cookie\)?\.',
75
        '\(?ScriptElement\)?\.',
76
        'd\s*a\s*t\s*a\s*:',
77
    ];
78
    
79
    /**
80
     * List of simple never allowed call statements.
81
     *
82
     * @var string[]
83
     */
84
    private $_never_allowed_call_strings = [
85
        // default javascript
86
        'javascript',
87
        // Java: jar-protocol is an XSS hazard
88
        'jar',
89
        // Mac (will not run the script, but open it in AppleScript Editor)
90
        'applescript',
91
        // IE: https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#VBscript_in_an_image
92
        'vbscript',
93
        'vbs',
94
        // IE, surprise!
95
        'wscript',
96
        // IE
97
        'jscript',
98
        // https://html5sec.org/#behavior
99
        'behavior',
100
        // old Netscape
101
        'mocha',
102
        // old Netscape
103
        'livescript',
104
        // default view source
105
        'view-source',
106
    ];
107

108
    /**
109
     * @var string[]
110
     */
111
    private $_never_allowed_str_afterwards = [
112
        '&lt;script&gt;',
113
        '&lt;/script&gt;',
114
    ];
115

116
    /**
117
     * List of never allowed strings, afterwards.
118
     *
119
     * @var string[]
120
     */
121
    private $_never_allowed_on_events_afterwards = [
122
        'onAbort',
123
        'onActivate',
124
        'onAttribute',
125
        'onAfterPrint',
126
        'onAfterScriptExecute',
127
        'onAfterUpdate',
128
        'onAnimationCancel',
129
        'onAnimationEnd',
130
        'onAnimationIteration',
131
        'onAnimationStart',
132
        'onAriaRequest',
133
        'onAutoComplete',
134
        'onAutoCompleteError',
135
        'onAuxClick',
136
        'onBeforeActivate',
137
        'onBeforeCopy',
138
        'onBeforeCut',
139
        'onBeforeInput',
140
        'onBeforePrint',
141
        'onBeforeDeactivate',
142
        'onBeforeEditFocus',
143
        'onBeforePaste',
144
        'onBeforePrint',
145
        'onBeforeScriptExecute',
146
        'onBeforeToggle',
147
        'onBeforeUnload',
148
        'onBeforeUpdate',
149
        'onBegin',
150
        'onBlur',
151
        'onBounce',
152
        'onCancel',
153
        'onCanPlay',
154
        'onCanPlayThrough',
155
        'onCellChange',
156
        'onChange',
157
        'onClick',
158
        'onClose',
159
        'onCommand',
160
        'onCompassNeedsCalibration',
161
        'onContextMenu',
162
        'onControlSelect',
163
        'onCopy',
164
        'onCueChange',
165
        'onCut',
166
        'onDataAvailable',
167
        'onDataSetChanged',
168
        'onDataSetComplete',
169
        'onDblClick',
170
        'onDeactivate',
171
        'onDeviceLight',
172
        'onDeviceMotion',
173
        'onDeviceOrientation',
174
        'onDeviceProximity',
175
        'onDrag',
176
        'onDragDrop',
177
        'onDragEnd',
178
        'onDragExit',
179
        'onDragEnter',
180
        'onDragLeave',
181
        'onDragOver',
182
        'onDragStart',
183
        'onDrop',
184
        'onDurationChange',
185
        'onEmptied',
186
        'onEnd',
187
        'onEnded',
188
        'onError',
189
        'onErrorUpdate',
190
        'onExit',
191
        'onFilterChange',
192
        'onFinish',
193
        'onFocus',
194
        'onFocusIn',
195
        'onFocusOut',
196
        'onFormChange',
197
        'onFormInput',
198
        'onFullScreenChange',
199
        'onFullScreenError',
200
        'onGotPointerCapture',
201
        'onHashChange',
202
        'onHelp',
203
        'onInput',
204
        'onInvalid',
205
        'onKeyDown',
206
        'onKeyPress',
207
        'onKeyUp',
208
        'onLanguageChange',
209
        'onLayoutComplete',
210
        'onLoad',
211
        'onLoadEnd',
212
        'onLoadedData',
213
        'onLoadedMetaData',
214
        'onLoadStart',
215
        'onLoseCapture',
216
        'onLostPointerCapture',
217
        'onMediaComplete',
218
        'onMediaError',
219
        'onMessage',
220
        'onMouseDown',
221
        'onMouseEnter',
222
        'onMouseLeave',
223
        'onMouseMove',
224
        'onMouseOut',
225
        'onMouseOver',
226
        'onMouseUp',
227
        'onMouseWheel',
228
        'onMove',
229
        'onMoveEnd',
230
        'onMoveStart',
231
        'onMozFullScreenChange',
232
        'onMozFullScreenError',
233
        'onMozPointerLockChange',
234
        'onMozPointerLockError',
235
        'onMsContentZoom',
236
        'onMsFullScreenChange',
237
        'onMsFullScreenError',
238
        'onMsGestureChange',
239
        'onMsGestureDoubleTap',
240
        'onMsGestureEnd',
241
        'onMsGestureHold',
242
        'onMsGestureStart',
243
        'onMsGestureTap',
244
        'onMsGotPointerCapture',
245
        'onMsInertiaStart',
246
        'onMsLostPointerCapture',
247
        'onMsManipulationStateChanged',
248
        'onMsPointerCancel',
249
        'onMsPointerDown',
250
        'onMsPointerEnter',
251
        'onMsPointerLeave',
252
        'onMsPointerMove',
253
        'onMsPointerOut',
254
        'onMsPointerOver',
255
        'onMsPointerUp',
256
        'onMsSiteModeJumpListItemRemoved',
257
        'onMsThumbnailClick',
258
        'onOffline',
259
        'onOnline',
260
        'onOutOfSync',
261
        'onPage',
262
        'onPageHide',
263
        'onPageShow',
264
        'onPaste',
265
        'onPause',
266
        'onPlay',
267
        'onPlaying',
268
        'onPointerCancel',
269
        'onPointerDown',
270
        'onPointerEnter',
271
        'onPointerLeave',
272
        'onPointerLockChange',
273
        'onPointerLockError',
274
        'onPointerMove',
275
        'onPointerOut',
276
        'onPointerOver',
277
        'onPointerRawUpdate',
278
        'onPointerUp',
279
        'onPopState',
280
        'onProgress',
281
        'onPropertyChange',
282
        'onqt_error',
283
        'onRateChange',
284
        'onReadyStateChange',
285
        'onReceived',
286
        'onRepeat',
287
        'onReset',
288
        'onResize',
289
        'onResizeEnd',
290
        'onResizeStart',
291
        'onResume',
292
        'onReverse',
293
        'onRowDelete',
294
        'onRowEnter',
295
        'onRowExit',
296
        'onRowInserted',
297
        'onRowsDelete',
298
        'onRowsEnter',
299
        'onRowsExit',
300
        'onRowsInserted',
301
        'onScroll',
302
        'onSearch',
303
        'onSeek',
304
        'onSeeked',
305
        'onSeeking',
306
        'onSelect',
307
        'onSelectionChange',
308
        'onSelectStart',
309
        'onStalled',
310
        'onStorage',
311
        'onStorageCommit',
312
        'onStart',
313
        'onStop',
314
        'onShow',
315
        'onSyncRestored',
316
        'onSubmit',
317
        'onSuspend',
318
        'onSynchRestored',
319
        'onTimeError',
320
        'onTimeUpdate',
321
        'onTimer',
322
        'onTrackChange',
323
        'onTransitionEnd',
324
        'onTransitionRun',
325
        'onTransitionStart',
326
        'onToggle',
327
        'onTouchCancel',
328
        'onTouchEnd',
329
        'onTouchLeave',
330
        'onTouchMove',
331
        'onTouchStart',
332
        'onTransitionCancel',
333
        'onTransitionEnd',
334
        'onUnload',
335
        'onUnhandledRejection',
336
        'onURLFlip',
337
        'onUserProximity',
338
        'onVolumeChange',
339
        'onWaiting',
340
        'onWebKitAnimationEnd',
341
        'onWebKitAnimationIteration',
342
        'onWebKitAnimationStart',
343
        'onWebKitFullScreenChange',
344
        'onWebKitFullScreenError',
345
        'onWebKitTransitionEnd',
346
        'onWheel',
347
    ];
348

349
    /**
350
     * https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Event_Handlers
351
     *
352
     * @var string[]
353
     */
354
    private $_evil_attributes_regex = [
355
        'style',
356
        'xmlns:xdp',
357
        'formaction',
358
        'form',
359
        'xlink:href',
360
        'seekSegmentTime',
361
        'FSCommand',
362
    ];
363

364
    /**
365
     * @var string[]
366
     */
367
    private $_evil_html_tags = [
368
        'applet',
369
        'audio',
370
        'basefont',
371
        'base',
372
        'behavior',
373
        'bgsound',
374
        'blink',
375
        'body',
376
        'embed',
377
        'eval',
378
        'expression',
379
        'form',
380
        'frameset',
381
        'frame',
382
        'head',
383
        'html',
384
        'ilayer',
385
        'iframe',
386
        'input',
387
        'button',
388
        'select',
389
        'isindex',
390
        'layer',
391
        'link',
392
        'meta',
393
        'keygen',
394
        'object',
395
        'plaintext',
396
        'style',
397
        'script',
398
        'textarea',
399
        'title',
400
        'math',
401
        'noscript',
402
        'event-source',
403
        'vmlframe',
404
        'video',
405
        'source',
406
        'svg',
407
        'xml',
408
    ];
409

410
    /**
411
     * @var string
412
     */
413
    private $_spacing_regex = '(?:\s|"|\'|\+|&#x0[9A-F];|%0[9a-f])*?';
414

415
    /**
416
     * The replacement-string for not allowed strings.
417
     *
418
     * @var string
419
     */
420
    private $_replacement = '';
421

422
    /**
423
     * List of never allowed strings.
424
     *
425
     * @var string[]
426
     */
427
    private $_never_allowed_str = [];
428

429
    /**
430
     * If your DB (MySQL) encoding is "utf8" and not "utf8mb4", then
431
     * you can't save 4-Bytes chars from UTF-8 and someone can create stored XSS-attacks.
432
     *
433
     * @var bool
434
     */
435
    private $_stripe_4byte_chars = false;
436

437
    /**
438
     * @var bool|null
439
     */
440
    private $_xss_found;
441

442
    /**
443
     * @var string
444
     */
445
    private $_cache_evil_attributes_regex_string = '';
446

447
    /**
448
     * @var string
449
     */
450
    private $_cache_never_allowed_regex_string = '';
451

452
    /**
453
     * @var string
454
     */
455
    private $_cache__evil_html_tags_str = '';
456
    
457
    public function __construct()
458
    {
459
        $this->_initNeverAllowedStr();
1,116✔
460
        $this->_initNeverAllowedRegex();
1,116✔
461
    }
496✔
462

463
    /**
464
     * Compact any exploded words.
465
     *
466
     * <p>
467
     * <br />
468
     * INFO: This corrects words like:  j a v a s c r i p t
469
     * <br />
470
     * These words are compacted back to their correct state.
471
     * </p>
472
     *
473
     * @param string $str
474
     *
475
     * @return string
476
     */
477
    private function _compact_exploded_javascript($str)
478
    {
479
        static $WORDS_CACHE;
1,116✔
480
        $WORDS_CACHE['chunk'] = [];
1,116✔
481
        $WORDS_CACHE['split'] = [];
1,116✔
482

483
        $words = [
620✔
484
            'javascript',
1,116✔
485
            '<script',
620✔
486
            '</script>',
620✔
487
            'base64',
620✔
488
            'document',
620✔
489
            'eval',
620✔
490
        ];
620✔
491

492
        // check if we need to perform the regex-stuff
493
        if (\strlen($str) <= 30) {
1,116✔
494
            $useStrPos = true;
783✔
495
        } else {
496
            $useStrPos = false;
954✔
497
        }
498

499
        foreach ($words as $word) {
1,116✔
500
            if (!isset($WORDS_CACHE['chunk'][$word])) {
1,116✔
501
                $WORDS_CACHE['chunk'][$word] = \substr(
1,116✔
502
                    \chunk_split($word, 1, $this->_spacing_regex),
1,116✔
503
                    0,
1,116✔
504
                    -\strlen($this->_spacing_regex)
1,116✔
505
                );
620✔
506

507
                $WORDS_CACHE['split'][$word] = \str_split($word);
1,116✔
508
            }
509

510
            if ($useStrPos) {
1,116✔
511
                foreach ($WORDS_CACHE['split'][$word] as $charTmp) {
783✔
512
                    if (\stripos($str, $charTmp) === false) {
783✔
513
                        continue 2;
783✔
514
                    }
515
                }
516
            }
517

518
            // We only want to do this when it is followed by a non-word character.
519
            // And if there are no char at the start of the string.
520
            //
521
            // That way valid stuff like "dealer to!" does not become "dealerto".
522

523
            $str = (string) \preg_replace_callback(
1,035✔
524
                '#(?<before>[^\p{L}]|^)(?<word>' . \str_replace(
1,035✔
525
                    ['#', '.'],
1,035✔
526
                    ['\#', '\.'],
1,035✔
527
                    $WORDS_CACHE['chunk'][$word]
1,035✔
528
                ) . ')(?<after>[^\p{L}@.!?\' ]|$)#ius',
1,035✔
529
                function ($matches) {
575✔
530
                    return $this->_compact_exploded_words_callback($matches);
567✔
531
                },
1,035✔
532
                $str
1,035✔
533
            );
575✔
534
        }
535

536
        return $str;
1,116✔
537
    }
538

539
    /**
540
     * Compact exploded words.
541
     *
542
     * <p>
543
     * <br />
544
     * INFO: Callback method for xss_clean() to remove whitespace from things like 'j a v a s c r i p t'.
545
     * </p>
546
     *
547
     * @param string[] $matches
548
     *
549
     * @return  string
550
     */
551
    private function _compact_exploded_words_callback($matches)
552
    {
553
        return $matches['before'] . \preg_replace(
567✔
554
            '/' . $this->_spacing_regex . '/ius',
567✔
555
            '',
567✔
556
            $matches['word']
567✔
557
        ) . $matches['after'];
567✔
558
    }
559

560
    /**
561
     * HTML-Entity decode callback.
562
     *
563
     * @param string[] $match
564
     *
565
     * @return string
566
     */
567
    private function _decode_entity($match)
568
    {
569
        // init
570
        $str = $match[0];
972✔
571

572
        // protect GET variables without XSS in URLs
573
        $needProtection = true;
972✔
574
        if (\strpos($str, '=') !== false) {
972✔
575
            $strCopy = $str;
792✔
576
            $matchesTmp = [];
792✔
577
            while (\preg_match("/[?|&]?[\p{L}\d_\-\[\]]+\s*=\s*([\"'])(?<attr>[^\1]*?)\\1/u", $strCopy, $matches)) {
792✔
578
                $matchesTmp[] = $matches;
594✔
579
                $strCopy = \str_replace($matches[0], '', $strCopy);
594✔
580

581
                if (\substr_count($strCopy, '"') <= 1 && \substr_count($strCopy, '\'') <= 1) {
594✔
582
                    break;
594✔
583
                }
584
            }
585

586
            if ($strCopy !== $str) {
792✔
587
                $needProtection = false;
594✔
588
                foreach ($matchesTmp as $matches) {
594✔
589
                    if (isset($matches['attr'])) {
594✔
590
                        $tmpAntiXss = clone $this;
594✔
591

592
                        $urlPartClean = $tmpAntiXss->xss_clean((string) $matches['attr']);
594✔
593

594
                        if ($tmpAntiXss->isXssFound() === true) {
594✔
595
                            $this->_xss_found = true;
396✔
596

597
                            $urlPartClean = \str_replace(['&lt;', '&gt;'], [self::VOKU_ANTI_XSS_LT, self::VOKU_ANTI_XSS_GT], $urlPartClean);
396✔
598
                            $urlPartClean = UTF8::rawurldecode($urlPartClean);
396✔
599
                            $urlPartClean = \str_replace([self::VOKU_ANTI_XSS_LT, self::VOKU_ANTI_XSS_GT], ['&lt;', '&gt;'], $urlPartClean);
396✔
600

601
                            $str = \str_ireplace($matches['attr'], $urlPartClean, $str);
440✔
602
                        }
603
                    }
604
                }
605
            }
606
        }
607

608
        if ($needProtection) {
972✔
609
            $str = \str_replace(['&lt;', '&gt;'], [self::VOKU_ANTI_XSS_LT, self::VOKU_ANTI_XSS_GT], $str);
711✔
610
            $str = $this->_entity_decode(UTF8::rawurldecode($str));
711✔
611
            $str = \str_replace([self::VOKU_ANTI_XSS_LT, self::VOKU_ANTI_XSS_GT], ['&lt;', '&gt;'], $str);
711✔
612
        }
613

614
        return $str;
972✔
615
    }
616

617
    /**
618
     * Decode the html-tags but keep links without XSS.
619
     *
620
     * @param string $str
621
     *
622
     * @return string
623
     */
624
    private function _decode_string($str)
625
    {
626
        // init
627
        $regExForHtmlTags = '/<\p{L}+(?:[^>"\']|(["\']).*\1)*>/usU';
1,116✔
628

629
        if (
630
            \strpos($str, '<') !== false
1,116✔
631
            &&
632
            \preg_match($regExForHtmlTags, $str)
1,116✔
633
        ) {
634
            $str = (string) \preg_replace_callback(
972✔
635
                $regExForHtmlTags,
972✔
636
                function ($matches) {
540✔
637
                    return $this->_decode_entity($matches);
972✔
638
                },
972✔
639
                $str
972✔
640
            );
540✔
641
        } else {
642
            $str = UTF8::rawurldecode($str);
891✔
643
        }
644

645
        return $str;
1,116✔
646
    }
647

648
    /**
649
     * @param string $str
650
     *
651
     * @return string
652
     */
653
    private function _do($str)
654
    {
655
        $str = (string) $str;
1,116✔
656
        $strInt = (int) $str;
1,116✔
657
        $strFloat = (float) $str;
1,116✔
658
        if (
659
            !$str
1,116✔
660
            ||
661
            (string) $strInt === $str
1,116✔
662
            ||
663
            (string) $strFloat === $str
1,116✔
664
        ) {
665
            // no xss found
666
            if ($this->_xss_found !== true) {
270✔
667
                $this->_xss_found = false;
225✔
668
            }
669

670
            return $str;
270✔
671
        }
672

673
        // remove the BOM from UTF-8 / UTF-16 / UTF-32 strings
674
        $str = UTF8::remove_bom($str);
1,116✔
675

676
        // replace the diamond question mark (�) and invalid-UTF8 chars
677
        $str = UTF8::replace_diamond_question_mark($str, '');
1,116✔
678

679
        // replace invisible characters with one single space
680
        $str = UTF8::remove_invisible_characters($str, true, '', false);
1,116✔
681

682
        // decode UTF-7 characters
683
        $str = $this->_repack_utf7($str);
1,116✔
684

685
        // decode the string
686
        $str = $this->_decode_string($str);
1,116✔
687

688
        // remove all >= 4-Byte chars if needed
689
        if ($this->_stripe_4byte_chars) {
1,116✔
690
            $str = (string) \preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $str);
9✔
691
        }
692

693
        // backup the string (for later comparison)
694
        $str_backup = $str;
1,116✔
695
        
696
        // process
697
        do {
698
            // backup the string (for the loop)
699
            $str_backup_loop = $str;
1,116✔
700

701
            // correct words before the browser will do it
702
            $str = $this->_compact_exploded_javascript($str);
1,116✔
703
    
704
            // remove disallowed javascript calls in links, images etc.
705
            $str = $this->_remove_disallowed_javascript($str);
1,116✔
706
    
707
            // remove strings that are never allowed
708
            $str = $this->_do_never_allowed($str);
1,116✔
709
    
710
            // remove evil attributes such as style, onclick and xmlns
711
            $str = $this->_remove_evil_attributes($str);
1,116✔
712
    
713
            // sanitize naughty JavaScript elements
714
            $str = $this->_sanitize_naughty_javascript($str);
1,116✔
715
    
716
            // sanitize naughty HTML elements
717
            $str = $this->_sanitize_naughty_html($str);
1,116✔
718
    
719
            // final clean up
720
            //
721
            // -> This adds a bit of extra precaution in case something got through the above filters.
722
            $str = $this->_do_never_allowed_afterwards($str);
1,116✔
723
        } while ($str_backup_loop !== $str);
1,116✔
724

725
        // check for xss
726
        if ($this->_xss_found !== true) {
1,116✔
727
            $this->_xss_found = !($str_backup === $str);
1,116✔
728
        }
729
        
730
        return $str;
1,116✔
731
    }
732

733
    /**
734
     * Remove never allowed strings.
735
     *
736
     * @param string $str
737
     *
738
     * @return string
739
     */
740
    private function _do_never_allowed($str)
741
    {
742
        static $NEVER_ALLOWED_CACHE = [];
1,116✔
743

744
        $NEVER_ALLOWED_CACHE['keys'] = null;
1,116✔
745

746
        if ($NEVER_ALLOWED_CACHE['keys'] === null) {
1,116✔
747
            $NEVER_ALLOWED_CACHE['keys'] = \array_keys($this->_never_allowed_str);
1,116✔
748
        }
749

750
        $str = \str_ireplace(
1,116✔
751
            $NEVER_ALLOWED_CACHE['keys'],
1,116✔
752
            $this->_never_allowed_str,
1,116✔
753
            $str
1,116✔
754
        );
620✔
755

756
        // ---
757

758
        $replaceNeverAllowedCall = [];
1,116✔
759
        foreach ($this->_never_allowed_call_strings as $call) {
1,116✔
760
            if (\stripos($str, $call) !== false) {
1,116✔
761
                $replaceNeverAllowedCall[] = $call;
535✔
762
            }
763
        }
764
        if (\count($replaceNeverAllowedCall) > 0) {
1,116✔
765
            $str = (string) \preg_replace(
369✔
766
                '#([^\p{L}]|^)(?:' . \implode('|', $replaceNeverAllowedCall) . ')\s*:(?:.*?([/\\\;()\'">]|$))#ius',
369✔
767
                '$1' . $this->_replacement . '$2',
369✔
768
                $str
369✔
769
            );
205✔
770
        }
771

772
        // ---
773

774
        $regex_combined = [];
1,116✔
775
        foreach ($this->_never_allowed_regex as $regex => $replacement) {
1,116✔
776
            if ($replacement === $this->_replacement) {
1,116✔
777
                $regex_combined[] = $regex;
1,116✔
778

779
                continue;
1,116✔
780
            }
781

782
            $str = (string) \preg_replace(
1,116✔
783
                '#' . $regex . '#iUus',
1,116✔
784
                $replacement,
1,116✔
785
                $str
1,116✔
786
            );
620✔
787
        }
788

789
        if (!$this->_cache_never_allowed_regex_string || $regex_combined !== []) {
1,116✔
790
            $this->_cache_never_allowed_regex_string = \implode('|', $regex_combined);
1,116✔
791
        }
792

793
        if ($this->_cache_never_allowed_regex_string) {
1,116✔
794
            $str = (string) \preg_replace(
1,116✔
795
                '#' . $this->_cache_never_allowed_regex_string . '#ius',
1,116✔
796
                $this->_replacement,
1,116✔
797
                $str
1,116✔
798
            );
620✔
799
        }
800

801
        return $str;
1,116✔
802
    }
803

804
    /**
805
     * @return array
806
     *
807
     * @phpstan-return array<string, list<string>>
808
     */
809
    private function _get_never_allowed_on_events_afterwards_chunks()
810
    {
811
        // init
812
        $array = [];
585✔
813

814
        foreach ($this->_never_allowed_on_events_afterwards as $event) {
585✔
815
            $array[$event[0] . $event[1] . $event[2]][] = $event;
585✔
816
        }
817

818
        return $array;
585✔
819
    }
820

821
    /**
822
     * Remove never allowed string, afterwards.
823
     *
824
     * <p>
825
     * <br />
826
     * INFO: clean-up also some string, if there is no html-tag
827
     * </p>
828
     *
829
     * @param string $str
830
     *
831
     * @return  string
832
     */
833
    private function _do_never_allowed_afterwards($str)
834
    {
835
        if (\stripos($str, 'on') !== false) {
1,116✔
836
            foreach ($this->_get_never_allowed_on_events_afterwards_chunks() as $eventNameBeginning => $events) {
585✔
837
                if (\stripos($str, $eventNameBeginning) === false) {
585✔
838
                    continue;
576✔
839
                }
840

841
                foreach ($events as $event) {
504✔
842
                    if (\stripos($str, $event) === false) {
504✔
843
                        continue;
495✔
844
                    }
845

846
                    $regex = '(?<before>[^\p{L}@.!?>]|^)(?:' . \implode('|', $events) . ')(?<after>\(.*?\)|.*?>|(?:\s|\[.*?\])*?=(?:\s|\[.*?\])*?|(?:\s|\[.*?\])*?&equals;(?:\s|\[.*?\])*?|[^\p{L}]*?=[^\p{L}]*?|[^\p{L}]*?&equals;[^\p{L}]*?|$|\s*?>*?$)';
234✔
847

848
                    do {
849
                        $count = $temp_count = 0;
234✔
850

851
                        $str = (string) \preg_replace(
234✔
852
                            '#' . $regex . '#ius',
234✔
853
                            '$1' . $this->_replacement . '$2',
234✔
854
                            $str,
234✔
855
                            -1,
234✔
856
                            $temp_count
234✔
857
                        );
130✔
858
                        $count += $temp_count;
234✔
859
                    } while ($count);
234✔
860

861
                    break;
294✔
862
                }
863
            }
864
        }
865

866
        return (string) \str_ireplace(
1,116✔
867
            $this->_never_allowed_str_afterwards,
1,116✔
868
            $this->_replacement,
1,116✔
869
            $str
1,116✔
870
        );
620✔
871
    }
872

873
    /**
874
     * Entity-decoding.
875
     *
876
     * @param string $str
877
     *
878
     * @return string
879
     */
880
    private function _entity_decode($str)
881
    {
882
        static $HTML_ENTITIES_CACHE;
711✔
883

884
        $flags = ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED | ENT_SUBSTITUTE;
711✔
885

886
        // decode-again, for e.g. HHVM or miss configured applications ...
887
        if (
888
            \strpos($str, '&') !== false
711✔
889
            &&
890
            \preg_match_all('/(?<html_entity>&[A-Za-z]{2,};{0})/', $str, $matches)
711✔
891
        ) {
892
            if ($HTML_ENTITIES_CACHE === null) {
27✔
893
                // links:
894
                // - http://dev.w3.org/html5/html-author/charref
895
                // - http://www.w3schools.com/charsets/ref_html_entities_n.asp
896
                $entitiesSecurity = [
5✔
897
                    '&#x00000;'          => '',
9✔
898
                    '&#0;'               => '',
5✔
899
                    '&#x00001;'          => '',
5✔
900
                    '&#1;'               => '',
5✔
901
                    '&nvgt;'             => '',
5✔
902
                    '&#61253;'           => '',
5✔
903
                    '&#x0EF45;'          => '',
5✔
904
                    '&shy;'              => '',
5✔
905
                    '&#x000AD;'          => '',
5✔
906
                    '&#173;'             => '',
5✔
907
                    '&colon;'            => ':',
5✔
908
                    '&#x0003A;'          => ':',
5✔
909
                    '&#58;'              => ':',
5✔
910
                    '&lpar;'             => '(',
5✔
911
                    '&#x00028;'          => '(',
5✔
912
                    '&#40;'              => '(',
5✔
913
                    '&rpar;'             => ')',
5✔
914
                    '&#x00029;'          => ')',
5✔
915
                    '&#41;'              => ')',
5✔
916
                    '&quest;'            => '?',
5✔
917
                    '&#x0003F;'          => '?',
5✔
918
                    '&#63;'              => '?',
5✔
919
                    '&sol;'              => '/',
5✔
920
                    '&#x0002F;'          => '/',
5✔
921
                    '&#47;'              => '/',
5✔
922
                    '&apos;'             => '\'',
5✔
923
                    '&#x00027;'          => '\'',
5✔
924
                    '&#039;'             => '\'',
5✔
925
                    '&#39;'              => '\'',
5✔
926
                    '&#x27;'             => '\'',
5✔
927
                    '&bsol;'             => '\'',
5✔
928
                    '&#x0005C;'          => '\\',
5✔
929
                    '&#92;'              => '\\',
5✔
930
                    '&comma;'            => ',',
5✔
931
                    '&#x0002C;'          => ',',
5✔
932
                    '&#44;'              => ',',
5✔
933
                    '&period;'           => '.',
5✔
934
                    '&#x0002E;'          => '.',
5✔
935
                    '&quot;'             => '"',
5✔
936
                    '&QUOT;'             => '"',
5✔
937
                    '&#x00022;'          => '"',
5✔
938
                    '&#34;'              => '"',
5✔
939
                    '&grave;'            => '`',
5✔
940
                    '&DiacriticalGrave;' => '`',
5✔
941
                    '&#x00060;'          => '`',
5✔
942
                    '&#96;'              => '`',
5✔
943
                    '&#46;'              => '.',
5✔
944
                    '&equals;'           => '=',
5✔
945
                    '&#x0003D;'          => '=',
5✔
946
                    '&#61;'              => '=',
5✔
947
                    '&newline;'          => "\n",
5✔
948
                    '&#x0000A;'          => "\n",
5✔
949
                    '&#10;'              => "\n",
5✔
950
                    '&tab;'              => "\t",
5✔
951
                    '&#x00009;'          => "\t",
5✔
952
                    '&#9;'               => "\t",
5✔
953
                ];
5✔
954

955
                $HTML_ENTITIES_CACHE = \array_merge(
9✔
956
                    $entitiesSecurity,
9✔
957
                    \array_flip(\get_html_translation_table(HTML_ENTITIES, $flags)),
9✔
958
                    \array_flip(self::_get_data('entities_fallback'))
9✔
959
                );
5✔
960
            }
961

962
            $search = [];
27✔
963
            $replace = [];
27✔
964
            foreach ($matches['html_entity'] as $match) {
27✔
965
                $match .= ';';
27✔
966
                if (isset($HTML_ENTITIES_CACHE[$match])) {
27✔
967
                    $search[$match] = $match;
9✔
968
                    $replace[$match] = $HTML_ENTITIES_CACHE[$match];
13✔
969
                }
970
            }
971

972
            if (\count($replace) > 0) {
27✔
973
                $str = \str_ireplace($search, $replace, $str);
9✔
974
            }
975
        }
976

977
        return $str;
711✔
978
    }
979

980
    /**
981
     * Filters tag attributes for consistency and safety.
982
     *
983
     * @param string $str
984
     *
985
     * @return string
986
     */
987
    private function _filter_attributes($str)
988
    {
989
        if ($str === '') {
414✔
990
            return '';
162✔
991
        }
992

993
        if (\strpos($str, '=') !== false) {
414✔
994
            $matchesTmp = [];
405✔
995
            while (\preg_match('#\s*[\p{L}\d_\-\[\]]+\s*=\s*(["\'])(?:[^\1]*?)\\1#u', $str, $matches)) {
405✔
996
                $matchesTmp[] = $matches[0];
342✔
997
                $str = \str_replace($matches[0], '', $str);
342✔
998

999
                if (\substr_count($str, '"') <= 1 && \substr_count($str, '\'') <= 1) {
342✔
1000
                    break;
342✔
1001
                }
1002
            }
1003
            $out = \implode('', $matchesTmp);
405✔
1004
        } else {
1005
            $out = $str;
108✔
1006
        }
1007

1008
        return $out;
414✔
1009
    }
1010

1011
    /**
1012
     * get data from "/data/*.php"
1013
     *
1014
     * @param string $file
1015
     *
1016
     * @return string[]
1017
     *
1018
     * @phpstan-return array<string, string>
1019
     */
1020
    private static function _get_data($file)
1021
    {
1022
        return include __DIR__ . '/data/' . $file . '.php';
9✔
1023
    }
1024

1025
    /**
1026
     * initialize "$this->_never_allowed_str"
1027
     *
1028
     * @return void
1029
     */
1030
    private function _initNeverAllowedStr()
1031
    {
1032
        $this->_never_allowed_str = [
1,116✔
1033
            'document.cookie'   => $this->_replacement,
1,116✔
1034
            '(document).cookie' => $this->_replacement,
1,116✔
1035
            'document.write'    => $this->_replacement,
1,116✔
1036
            '(document).write'  => $this->_replacement,
1,116✔
1037
            '.parentNode'       => $this->_replacement,
1,116✔
1038
            '.innerHTML'        => $this->_replacement,
1,116✔
1039
            '.appendChild'      => $this->_replacement,
1,116✔
1040
            '-moz-binding'      => $this->_replacement,
1,116✔
1041
            '<?'                => '&lt;?',
1,116✔
1042
            '?>'                => '?&gt;',
1,116✔
1043
            '<![CDATA['         => '&lt;![CDATA[',
1,116✔
1044
            '<!ENTITY'          => '&lt;!ENTITY',
1,116✔
1045
            '<!DOCTYPE'         => '&lt;!DOCTYPE',
1,116✔
1046
            '<!ATTLIST'         => '&lt;!ATTLIST',
1,116✔
1047
        ];
620✔
1048
    }
496✔
1049

1050
    /**
1051
     * initialize "$this->_never_allowed_regex"
1052
     *
1053
     * @return void
1054
     */
1055
    private function _initNeverAllowedRegex()
1056
    {
1057
        $this->_never_allowed_regex = [
1,116✔
1058
            // default javascript
1059
            '(\(?:?document\)?|\(?:?window\)?(?:\.document)?)\.(?:location|on\w*)' => $this->_replacement,
1,116✔
1060
            // data-attribute + base64
1061
            "([\"'])?data\s*:\s*(?!image\s*\/\s*(?!svg.*?))[^\1]*?base64[^\1]*?,[^\1]*?\1?" => $this->_replacement,
1,116✔
1062
            // old IE, old Netscape
1063
            'expression\s*(?:\(|&\#40;)' => $this->_replacement,
1,116✔
1064
            // src="js"
1065
            'src\=(?<wrapper>[\'|"]).*\.js(?:\g{wrapper})' => $this->_replacement,
1,116✔
1066
            // comments
1067
            '<!--(.*)-->' => '&lt;!--$1--&gt;',
1,116✔
1068
            '<!--'        => '&lt;!--',
1,116✔
1069
        ];
620✔
1070
    }
496✔
1071

1072
    /**
1073
     * Callback method for xss_clean() to sanitize links.
1074
     *
1075
     * <p>
1076
     * <br />
1077
     * INFO: This limits the PCRE backtracks, making it more performance friendly
1078
     * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
1079
     * PHP 5.2+ on link-heavy strings.
1080
     * </p>
1081
     *
1082
     * @param string[] $match
1083
     *
1084
     * @return string
1085
     */
1086
    private function _js_link_removal_callback($match)
1087
    {
1088
        return $this->_js_removal_callback($match, 'href');
261✔
1089
    }
1090

1091
    /**
1092
     * Callback method for xss_clean() to sanitize tags.
1093
     *
1094
     * <p>
1095
     * <br />
1096
     * INFO: This limits the PCRE backtracks, making it more performance friendly
1097
     * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
1098
     * PHP 5.2+ on image tag heavy strings.
1099
     * </p>
1100
     *
1101
     * @param string[]  $match
1102
     * @param string $search
1103
     *
1104
     * @return string
1105
     */
1106
    private function _js_removal_callback($match, $search)
1107
    {
1108
        if (!$match[0]) {
414✔
1109
            return '';
×
1110
        }
1111

1112
        $replacer = $this->_filter_attributes($match[1]);
414✔
1113

1114
        // filter for "$search"-attributes
1115
        if (\stripos($match[1], $search . '=') !== false) {
414✔
1116
            $pattern = '#' . $search . '=(?<wrapper>[\'|"])(?<link>.*)(?:\g{wrapper})#isU';
387✔
1117
            $matchInner = [];
387✔
1118
            $foundSomethingBad = false;
387✔
1119
            if (\preg_match($pattern, $match[1], $matchInner)) {
387✔
1120
                $needProtection = true;
315✔
1121
                $matchInner['link'] = \str_replace(' ', '%20', $matchInner['link']);
315✔
1122

1123
                if (
1124
                    \strpos($matchInner[0], 'script') === false
315✔
1125
                    &&
1126
                    \strpos(\str_replace(['http://', 'https://'], '', $matchInner[0]), ':') === false
315✔
1127
                    &&
1128
                    (
1129
                        \filter_var($matchInner['link'], \FILTER_VALIDATE_URL) !== false
315✔
1130
                        ||
175✔
1131
                        \filter_var('https://localhost.localdomain/' . $matchInner['link'], \FILTER_VALIDATE_URL) !== false
315✔
1132
                    )
1133
                ) {
1134
                    $needProtection = false;
315✔
1135
                }
1136

1137
                if ($needProtection) {
315✔
1138
                    $tmpAntiXss = clone $this;
99✔
1139

1140
                    $tmpAntiXss->xss_clean((string) $matchInner[0]);
99✔
1141

1142
                    if ($tmpAntiXss->isXssFound() === true) {
99✔
1143
                        $foundSomethingBad = true;
63✔
1144
                        $this->_xss_found = true;
63✔
1145

1146
                        $replacer = (string) \preg_replace(
63✔
1147
                            $pattern,
63✔
1148
                            $search . '="' . $this->_replacement . '"',
63✔
1149
                            $replacer
63✔
1150
                        );
35✔
1151
                    }
1152
                }
1153
            }
1154

1155
            if (!$foundSomethingBad) {
387✔
1156
                // filter for javascript
1157
                $patternTmp = '';
387✔
1158
                foreach ($this->_never_allowed_call_strings as $callTmp) {
387✔
1159
                    if (\stripos($match[0], $callTmp) !== false) {
387✔
1160
                        $patternTmp .= $callTmp . ':|';
163✔
1161
                    }
1162
                }
1163
                $pattern = '#' . $search . '=.*(?:' . $patternTmp . \implode('|', $this->_never_allowed_js_callback_regex) . ')#ius';
387✔
1164
                $matchInner = [];
387✔
1165
                if (\preg_match($pattern, $match[1], $matchInner)) {
387✔
1166
                    $replacer = (string) \preg_replace(
126✔
1167
                        $pattern,
126✔
1168
                        $search . '="' . $this->_replacement . '"',
126✔
1169
                        $replacer
126✔
1170
                    );
70✔
1171
                }
1172
            }
1173
        }
1174
        
1175
        if (
1176
            \substr($match[0], -3) === ' />'
414✔
1177
            &&
1178
            \substr($match[1], -2) === ' /'
414✔
1179
            &&
1180
            \substr($replacer, -2) !== ' /'
414✔
1181
        ) {
1182
            $replacer .= ' /';
18✔
1183
        } elseif (
1184
            \substr($match[0], -2) === '/>'
414✔
1185
            &&
1186
            \substr($match[1], -1) === '/'
414✔
1187
            &&
1188
            \substr($replacer, -1) !== '/'
414✔
1189
        ) {
1190
            $replacer .= '/';
45✔
1191
        }
1192
        
1193
        
1194
        return \str_ireplace($match[1], $replacer, (string) $match[0]);
414✔
1195
    }
1196

1197
    /**
1198
     * Callback method for xss_clean() to sanitize image tags.
1199
     *
1200
     * <p>
1201
     * <br />
1202
     * INFO: This limits the PCRE backtracks, making it more performance friendly
1203
     * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
1204
     * PHP 5.2+ on image tag heavy strings.
1205
     * </p>
1206
     *
1207
     * @param string[] $match
1208
     *
1209
     * @return string
1210
     */
1211
    private function _js_src_removal_callback(array $match)
1212
    {
1213
        return $this->_js_removal_callback($match, 'src');
270✔
1214
    }
1215

1216
    /**
1217
     * Remove disallowed Javascript in links or img tags
1218
     *
1219
     * <p>
1220
     * <br />
1221
     * We used to do some version comparisons and use of stripos(),
1222
     * but it is dog slow compared to these simplified non-capturing
1223
     * preg_match(), especially if the pattern exists in the string
1224
     * </p>
1225
     *
1226
     * <p>
1227
     * <br />
1228
     * Note: It was reported that not only space characters, but all in
1229
     * the following pattern can be parsed as separators between a tag name
1230
     * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
1231
     * ... however, UTF8::clean() above already strips the
1232
     * hex-encoded ones, so we'll skip them below.
1233
     * </p>
1234
     *
1235
     * @param string $str
1236
     *
1237
     * @return string
1238
     */
1239
    private function _remove_disallowed_javascript($str)
1240
    {
1241
        do {
1242
            $original = $str;
1,116✔
1243

1244
            if (\stripos($str, '<a') !== false) {
1,116✔
1245
                $strTmp = \preg_replace_callback(
279✔
1246
                    '#<a[^\p{L}@>]+([^>]*?)(?:>|$)#iu',
279✔
1247
                    function ($matches) {
155✔
1248
                        return $this->_js_link_removal_callback($matches);
261✔
1249
                    },
279✔
1250
                    $str
279✔
1251
                );
155✔
1252
                if ($strTmp === null) {
279✔
1253
                    $strTmp = \preg_replace_callback(
×
1254
                        '#<a[^\p{L}@>]+([^>]*)(?:>|$)#iu',
×
1255
                        function ($matches) {
1256
                            return $this->_js_link_removal_callback($matches);
×
1257
                        },
×
1258
                        $str
×
1259
                    );
1260
                }
1261
                $str = (string)$strTmp;
279✔
1262
            }
1263

1264
            if (\stripos($str, '<img') !== false) {
1,116✔
1265
                $strTmp = \preg_replace_callback(
279✔
1266
                    '#<img[^\p{L}@]+([^>]*?)(?:\s?/?>|$)#iu',
279✔
1267
                    function ($matches) {
155✔
1268
                        if (
1269
                            \strpos($matches[1], 'base64') !== false
270✔
1270
                            &&
1271
                            \preg_match("/([\"'])?data\s*:\s*(?:image\s*\/.*)[^\1]*base64[^\1]*,[^\1]*\1?/iUus", $matches[1])
270✔
1272
                        ) {
1273
                            return $matches[0];
18✔
1274
                        }
1275

1276
                        return $this->_js_src_removal_callback($matches);
261✔
1277
                    },
279✔
1278
                    $str
279✔
1279
                );
155✔
1280
                if ($strTmp === null) {
279✔
1281
                    $strTmp = (string) \preg_replace_callback(
9✔
1282
                        '#<img[^\p{L}@]+([^>]*)(?:\s?/?>|$)#iu',
9✔
1283
                        function ($matches) {
5✔
1284
                            if (
1285
                                \strpos($matches[1], 'base64') !== false
9✔
1286
                                &&
1287
                                \preg_match("/([\"'])?data\s*:\s*(?:image\s*\/.*)[^\1]*base64[^\1]*,[^\1]*\1?/iUus", $matches[1])
9✔
1288
                            ) {
1289
                                return $matches[0];
9✔
1290
                            }
1291

1292
                            return $this->_js_src_removal_callback($matches);
×
1293
                        },
9✔
1294
                        $str
9✔
1295
                    );
5✔
1296
                }
1297
                $str = (string)$strTmp;
279✔
1298
            }
1299

1300
            if (\stripos($str, '<audio') !== false) {
1,116✔
1301
                $strTmp = \preg_replace_callback(
36✔
1302
                    '#<audio[^\p{L}@]+([^>]*?)(?:\s?/?>|$)#iu',
36✔
1303
                    function ($matches) {
20✔
1304
                        return $this->_js_src_removal_callback($matches);
36✔
1305
                    },
36✔
1306
                    $str
36✔
1307
                );
20✔
1308
                if ($strTmp === null) {
36✔
1309
                    $strTmp = (string) \preg_replace_callback(
×
1310
                        '#<audio[^\p{L}@]+([^>]*)(?:\s?/?>|$)#iu',
×
1311
                        function ($matches) {
1312
                            return $this->_js_src_removal_callback($matches);
×
1313
                        },
×
1314
                        $str
×
1315
                    );
1316
                }
1317
                $str = (string)$strTmp;
36✔
1318
            }
1319

1320
            if (\stripos($str, '<video') !== false) {
1,116✔
1321
                $strTmp = \preg_replace_callback(
63✔
1322
                    '#<video[^\p{L}@]+([^>]*?)(?:\s?/?>|$)#iu',
63✔
1323
                    function ($matches) {
35✔
1324
                        return $this->_js_src_removal_callback($matches);
54✔
1325
                    },
63✔
1326
                    $str
63✔
1327
                );
35✔
1328
                if ($strTmp === null) {
63✔
1329
                    $strTmp = \preg_replace_callback(
×
1330
                        '#<video[^\p{L}@]+([^>]*)(?:\s?/?>|$)#iu',
×
1331
                        function ($matches) {
1332
                            return $this->_js_src_removal_callback($matches);
×
1333
                        },
×
1334
                        $str
×
1335
                    );
1336
                }
1337
                $str = (string)$strTmp;
63✔
1338
            }
1339

1340
            if (\stripos($str, '<source') !== false) {
1,116✔
1341
                $str = (string) \preg_replace_callback(
45✔
1342
                    '#<source[^\p{L}@]+([^>]*)(?:\s?/?>|$)#iu',
45✔
1343
                    function ($matches) {
25✔
1344
                        return $this->_js_src_removal_callback($matches);
45✔
1345
                    },
45✔
1346
                    $str
45✔
1347
                );
25✔
1348
            }
1349

1350
            if (\stripos($str, 'script') !== false) {
1,116✔
1351
                // INFO: US-ASCII: ¼ === <
1352
                $str = (string) \preg_replace(
549✔
1353
                    '#(?:%3C|¼|<)\s*script[^\p{L}@]+(?:[^>]*)(?:\s?/?(?:%3E|¾|>)|$)#iu',
549✔
1354
                    $this->_replacement,
549✔
1355
                    $str
549✔
1356
                );
305✔
1357
            }
1358

1359
            if (\stripos($str, 'script') !== false) {
1,116✔
1360
                // INFO: US-ASCII: ¼ === <
1361
                $str = (string) \preg_replace(
468✔
1362
                    '#(?:%3C|¼|<)[^\p{L}@]*/*[^\p{L}@]*(?:script[^\p{L}@]+).*(?:%3E|¾|>)?#iUus',
468✔
1363
                    $this->_replacement,
468✔
1364
                    $str
468✔
1365
                );
260✔
1366
            }
1367
        } while ($original !== $str);
1,116✔
1368

1369
        return (string) $str;
1,116✔
1370
    }
1371

1372
    /**
1373
     * Remove Evil HTML Attributes (like event handlers and style).
1374
     *
1375
     * It removes the evil attribute and either:
1376
     *
1377
     *  - Everything up until a space. For example, everything between the pipes:
1378
     *
1379
     * <code>
1380
     *   <a |style=document.write('hello');alert('world');| class=link>
1381
     * </code>
1382
     *
1383
     *  - Everything inside the quotes. For example, everything between the pipes:
1384
     *
1385
     * <code>
1386
     *   <a |style="document.write('hello'); alert('world');"| class="link">
1387
     * </code>
1388
     *
1389
     * @param string $str <p>The string to check.</p>
1390
     *
1391
     * @return string
1392
     *                <p>The string with the evil attributes removed.</p>
1393
     */
1394
    private function _remove_evil_attributes($str)
1395
    {
1396
        // replace style-attribute, first (if needed)
1397
        if (
1398
            \stripos($str, 'style') !== false
1,116✔
1399
            &&
1400
            \in_array('style', $this->_evil_attributes_regex, true)
1,116✔
1401
        ) {
1402
            do {
1403
                $count = $temp_count = 0;
189✔
1404

1405
                $str = (string) \preg_replace(
189✔
1406
                    '/(<[^>]+)(?<!\p{L})(style\s*=\s*"(?:[^"]*?)"|style\s*=\s*\'(?:[^\']*?)\')/iu',
189✔
1407
                    '$1' . $this->_replacement,
189✔
1408
                    $str,
189✔
1409
                    -1,
189✔
1410
                    $temp_count
189✔
1411
                );
105✔
1412
                $count += $temp_count;
189✔
1413
            } while ($count);
189✔
1414
        }
1415

1416
        if (!$this->_cache_evil_attributes_regex_string) {
1,116✔
1417
            $this->_cache_evil_attributes_regex_string = \implode('|', $this->_evil_attributes_regex);
1,116✔
1418
            $this->_cache_evil_attributes_regex_string .= '|' . \implode('\w*|', $this->_never_allowed_on_events_afterwards);
1,116✔
1419
        }
1420

1421
        do {
1422
            $count = $temp_count = 0;
1,116✔
1423

1424
            // find occurrences of illegal attribute strings with and without quotes (" and ' are octal quotes)
1425
            $regex = '/(.*)((?:<[^>]+)(?<!\p{L}))(?:' . $this->_cache_evil_attributes_regex_string . ')(?:\s*=\s*)(?:\'(?:.*?)\'|"(?:.*?)")(.*)/ius';
1,116✔
1426
            $strTmp = \preg_replace(
1,116✔
1427
                $regex,
1,116✔
1428
                '$1$2' . $this->_replacement . '$3$4',
1,116✔
1429
                $str,
1,116✔
1430
                -1,
1,116✔
1431
                $temp_count
1,116✔
1432
            );
620✔
1433
            if ($strTmp === null) {
1,116✔
1434
                $regex = '/(?:' . $this->_cache_evil_attributes_regex_string . ')(?:\s*=\s*)(?:\'(?:.*?)\'|"(?:.*?)")/ius';
9✔
1435
                $strTmp = \preg_replace(
9✔
1436
                    $regex,
9✔
1437
                    $this->_replacement,
9✔
1438
                    $str,
9✔
1439
                    -1,
9✔
1440
                    $temp_count
9✔
1441
                );
5✔
1442
            }
1443
            $str = (string)$strTmp;
1,116✔
1444
            $count += $temp_count;
1,116✔
1445

1446
            $regex =  '/(.*?)(<[^>]+)(?<!\p{L})(?:' . $this->_cache_evil_attributes_regex_string . ')\s*=\s*(?:[^\s>]*)/ius';
1,116✔
1447
            $strTmp = \preg_replace(
1,116✔
1448
                $regex,
1,116✔
1449
                '$1$2' . $this->_replacement . '$3',
1,116✔
1450
                $str,
1,116✔
1451
                -1,
1,116✔
1452
                $temp_count
1,116✔
1453
            );
620✔
1454
            if ($strTmp === null) {
1,116✔
1455
                $regex =  '/(?<!\p{L})(?:' . $this->_cache_evil_attributes_regex_string . ')\s*=\s*(?:[^\s>]*)(.*?)/ius';
9✔
1456
                $strTmp = \preg_replace(
9✔
1457
                    $regex,
9✔
1458
                    '$1$2' . $this->_replacement . '$3',
9✔
1459
                    $str,
9✔
1460
                    -1,
9✔
1461
                    $temp_count
9✔
1462
                );
5✔
1463
            }
1464
            $str = (string)$strTmp;
1,116✔
1465
            $count += $temp_count;
1,116✔
1466
        } while ($count);
1,116✔
1467

1468
        return (string) $str;
1,116✔
1469
    }
1470

1471
    /**
1472
     * UTF-7 decoding function.
1473
     *
1474
     * @param string $str <p>HTML document for recode ASCII part of UTF-7 back to ASCII.</p>
1475
     *
1476
     * @return string
1477
     */
1478
    private function _repack_utf7($str)
1479
    {
1480
        if (\strpos($str, '-') === false) {
1,116✔
1481
            return $str;
972✔
1482
        }
1483

1484
        return (string) \preg_replace_callback(
450✔
1485
            '#\+([\p{L}\d]+)-#iu',
450✔
1486
            function ($matches) {
250✔
1487
                return $this->_repack_utf7_callback($matches);
36✔
1488
            },
450✔
1489
            $str
450✔
1490
        );
250✔
1491
    }
1492

1493
    /**
1494
     * Additional UTF-7 decoding function.
1495
     *
1496
     * @param string[] $strings <p>Array of strings for recode ASCII part of UTF-7 back to ASCII.</p>
1497
     *
1498
     * @return string
1499
     */
1500
    private function _repack_utf7_callback($strings)
1501
    {
1502
        $strTmp = \base64_decode($strings[1], true);
36✔
1503

1504
        if ($strTmp === false) {
36✔
1505
            return $strings[0];
×
1506
        }
1507

1508
        if (\rtrim(\base64_encode($strTmp), '=') !== \rtrim($strings[1], '=')) {
36✔
1509
            return $strings[0];
9✔
1510
        }
1511

1512
        $string = (string) \preg_replace_callback(
27✔
1513
            '/^((?:\x00.)*?)((?:[^\x00].)+)/us',
27✔
1514
            function ($matches) {
15✔
1515
                return $this->_repack_utf7_callback_back($matches);
×
1516
            },
27✔
1517
            $strTmp
27✔
1518
        );
15✔
1519

1520
        return (string) \preg_replace(
27✔
1521
            '/\x00(.)/us',
27✔
1522
            '$1',
27✔
1523
            $string
27✔
1524
        );
15✔
1525
    }
1526

1527
    /**
1528
     * Additional UTF-7 encoding function.
1529
     *
1530
     * @param string $str <p>String for recode ASCII part of UTF-7 back to ASCII.</p>
1531
     *
1532
     * @return string
1533
     */
1534
    private function _repack_utf7_callback_back($str)
1535
    {
1536
        return $str[1] . '+' . \rtrim(\base64_encode($str[2]), '=') . '-';
×
1537
    }
1538

1539
    /**
1540
     * Sanitize naughty HTML elements.
1541
     *
1542
     * <p>
1543
     * <br />
1544
     *
1545
     * If a tag containing any of the words in the list
1546
     * below is found, the tag gets converted to entities.
1547
     *
1548
     * <br /><br />
1549
     *
1550
     * So this: <blink>
1551
     * <br />
1552
     * Becomes: &lt;blink&gt;
1553
     * </p>
1554
     *
1555
     * @param string $str
1556
     *
1557
     * @return string
1558
     */
1559
    private function _sanitize_naughty_html($str)
1560
    {
1561
        // init
1562
        $strEnd = '';
1,116✔
1563

1564
        do {
1565
            $original = $str;
1,116✔
1566

1567
            if (
1568
                \strpos($str, '<') === false
1,116✔
1569
                &&
1570
                \strpos($str, '>') === false
1,116✔
1571
            ) {
1572
                return $str;
855✔
1573
            }
1574

1575
            if (!$this->_cache__evil_html_tags_str) {
963✔
1576
                $this->_cache__evil_html_tags_str = \implode('|', $this->_evil_html_tags);
963✔
1577
            }
1578

1579
            $str = (string) \preg_replace_callback(
963✔
1580
                '#<(?<start>/*\s*)(?<tagName>' . $this->_cache__evil_html_tags_str . ')(?<end>[^><]*)(?<rest>[><]*)#ius',
963✔
1581
                function ($matches) {
535✔
1582
                    return $this->_sanitize_naughty_html_callback($matches);
468✔
1583
                },
963✔
1584
                $str
963✔
1585
            );
535✔
1586

1587
            if (\strpos($str, '<') === false) {
963✔
1588
                return $str;
387✔
1589
            }
1590

1591
            if (
1592
                $this->_xss_found
801✔
1593
                &&
1594
                \trim($str) === '<'
801✔
1595
            ) {
1596
                return '';
18✔
1597
            }
1598

1599
            $str = (string) \preg_replace_callback(
801✔
1600
                '#<(?!!--|!\[)((?<start>/*\s*)((?<tagName>[\p{L}:]+)(?=[^\p{L}]|$|)|.+)[^\s"\'\p{L}>/=]*[^>]*)(?<closeTag>>)?#iusS', // tags without comments
801✔
1601
                function ($matches) {
445✔
1602
                    if (
1603
                        $this->_do_not_close_html_tags !== []
801✔
1604
                        &&
1605
                        isset($matches['tagName'])
801✔
1606
                        &&
1607
                        \in_array($matches['tagName'], $this->_do_not_close_html_tags, true)
801✔
1608
                    ) {
1609
                        return $matches[0];
9✔
1610
                    }
1611

1612
                    return $this->_close_html_callback($matches);
801✔
1613
                },
801✔
1614
                $str
801✔
1615
            );
445✔
1616

1617
            if ($str === $strEnd) {
801✔
1618
                return (string) $str;
198✔
1619
            }
1620

1621
            $strEnd = $str;
801✔
1622
        } while ($original !== $str);
801✔
1623

1624
        return (string) $str;
702✔
1625
    }
1626

1627
    /**
1628
     * @param string[] $matches
1629
     *
1630
     * @return string
1631
     */
1632
    private function _close_html_callback($matches)
1633
    {
1634
        if (empty($matches['closeTag'])) {
801✔
1635
            // allow e.g. "< $2.20" and e.g. "< 1 year"
1636
            if (\preg_match('/^[ .,\d=%€$₢₣£₤₶ℳ₥₦₧₨රුரூ௹रू₹૱₩₪₸₫֏₭₺₼₮₯₰₷₱﷼₲₾₳₴₽₵₡¢¥円৳元៛₠¤฿؋]*$|^[ .,\d=%€$₢₣£₤₶ℳ₥₦₧₨රුரூ௹रू₹૱₩₪₸₫֏₭₺₼₮₯₰₷₱﷼₲₾₳₴₽₵₡¢¥円৳元៛₠¤฿؋]+\p{L}*\s*$/u', $matches[1])) {
189✔
1637
                return '<' . \str_replace(['>', '<'], ['&gt;', '&lt;'], $matches[1]);
18✔
1638
            }
1639

1640
            return '&lt;' . \str_replace(['>', '<'], ['&gt;', '&lt;'], $matches[1]);
180✔
1641
        }
1642

1643
        return '<' . \str_replace(['>', '<'], ['&gt;', '&lt;'], $matches[1]) . '>';
756✔
1644
    }
1645

1646
    /**
1647
     * Sanitize naughty HTML.
1648
     *
1649
     * <p>
1650
     * <br />
1651
     * Callback method for AntiXSS->sanitize_naughty_html() to remove naughty HTML elements.
1652
     * </p>
1653
     *
1654
     * @param string[] $matches
1655
     *
1656
     * @return string
1657
     */
1658
    private function _sanitize_naughty_html_callback($matches)
1659
    {
1660
        $fullMatch = $matches[0];
468✔
1661

1662
        // skip some edge-cases
1663
        /** @noinspection NotOptimalIfConditionsInspection */
1664
        if (
1665
            (
1666
                \strpos($fullMatch, '=') === false
468✔
1667
                &&
260✔
1668
                \strpos($fullMatch, ' ') === false
468✔
1669
                &&
260✔
1670
                \strpos($fullMatch, ':') === false
468✔
1671
                &&
260✔
1672
                \strpos($fullMatch, '/') === false
468✔
1673
                &&
260✔
1674
                \strpos($fullMatch, '\\') === false
468✔
1675
                &&
260✔
1676
                \stripos($fullMatch, '<' . $matches['tagName'] . '>') !== 0
468✔
1677
                &&
260✔
1678
                \stripos($fullMatch, '</' . $matches['tagName'] . '>') !== 0
468✔
1679
                &&
260✔
1680
                \stripos($fullMatch, '<' . $matches['tagName'] . '<') !== 0
284✔
1681
            )
1682
            ||
1683
            \preg_match('/<\/?' . $matches['tagName'] . '\p{L}+>/ius', $fullMatch) === 1
468✔
1684
        ) {
1685
            return $fullMatch;
63✔
1686
        }
1687

1688
        return '&lt;' . $matches['start'] . $matches['tagName'] . $matches['end'] // encode opening brace
441✔
1689
               // encode captured opening or closing brace to prevent recursive vectors
245✔
1690
               . \str_replace(
441✔
1691
                   [
245✔
1692
                       '>',
441✔
1693
                   ],
245✔
1694
                   [
245✔
1695
                       '&gt;',
441✔
1696
                   ],
245✔
1697
                   $matches['rest']
441✔
1698
               );
245✔
1699
    }
1700

1701
    /**
1702
     * Sanitize naughty scripting elements
1703
     *
1704
     * <p>
1705
     * <br />
1706
     *
1707
     * Similar to above, only instead of looking for
1708
     * tags it looks for PHP and JavaScript commands
1709
     * that are disallowed. Rather than removing the
1710
     * code, it simply converts the parenthesis to entities
1711
     * rendering the code un-executable.
1712
     *
1713
     * <br /><br />
1714
     *
1715
     * For example:  <pre>eval('some code')</pre>
1716
     * <br />
1717
     * Becomes:      <pre>eval&#40;'some code'&#41;</pre>
1718
     * </p>
1719
     *
1720
     * @param string $str
1721
     *
1722
     * @return string
1723
     */
1724
    private function _sanitize_naughty_javascript($str)
1725
    {
1726
        if (\strpos($str, '(') !== false) {
1,116✔
1727
            $patterns = [
305✔
1728
                'alert',
549✔
1729
                'prompt',
305✔
1730
                'confirm',
305✔
1731
                'cmd',
305✔
1732
                'passthru',
305✔
1733
                'eval',
305✔
1734
                'exec',
305✔
1735
                'execScript',
305✔
1736
                'setTimeout',
305✔
1737
                'setInterval',
305✔
1738
                'setImmediate',
305✔
1739
                'expression',
305✔
1740
                'system',
305✔
1741
                'fopen',
305✔
1742
                'fsockopen',
305✔
1743
                'file',
305✔
1744
                'file_get_contents',
305✔
1745
                'readfile',
305✔
1746
                'unlink',
305✔
1747
            ];
305✔
1748

1749
            $found = false;
549✔
1750
            foreach ($patterns as $pattern) {
549✔
1751
                if (\strpos($str, $pattern) !== false) {
549✔
1752
                    $found = true;
396✔
1753

1754
                    break;
430✔
1755
                }
1756
            }
1757

1758
            if ($found === true) {
549✔
1759
                $str = (string) \preg_replace(
396✔
1760
                    '#(?<!\p{L})(' . \implode('|', $patterns) . ')(\s*)\((.*)\)#uisU',
396✔
1761
                    '\\1\\2&#40;\\3&#41;',
396✔
1762
                    $str
396✔
1763
                );
220✔
1764
            }
1765
        }
1766

1767
        return (string) $str;
1,116✔
1768
    }
1769

1770
    /**
1771
     * Add some strings to the "_evil_attributes"-array.
1772
     *
1773
     * @param string[] $strings
1774
     *
1775
     * @return $this
1776
     */
1777
    public function addEvilAttributes(array $strings): self
1778
    {
1779
        if ($strings === []) {
18✔
1780
            return $this;
×
1781
        }
1782

1783
        // reset
1784
        $this->_cache_evil_attributes_regex_string = '';
18✔
1785

1786
        $this->_evil_attributes_regex = \array_merge(
18✔
1787
            $strings,
18✔
1788
            $this->_evil_attributes_regex
18✔
1789
        );
10✔
1790

1791
        return $this;
18✔
1792
    }
1793

1794
    /**
1795
     * Add some strings to the "_evil_html_tags"-array.
1796
     *
1797
     * @param string[] $strings
1798
     *
1799
     * @return $this
1800
     */
1801
    public function addEvilHtmlTags(array $strings): self
1802
    {
1803
        if ($strings === []) {
9✔
1804
            return $this;
×
1805
        }
1806

1807
        // reset
1808
        $this->_cache__evil_html_tags_str = '';
9✔
1809

1810
        $this->_evil_html_tags = \array_merge(
9✔
1811
            $strings,
9✔
1812
            $this->_evil_html_tags
9✔
1813
        );
5✔
1814

1815
        return $this;
9✔
1816
    }
1817

1818
    /**
1819
     * Add some strings to the "_never_allowed_regex"-array.
1820
     *
1821
     * @param string[] $strings
1822
     *
1823
     * @return $this
1824
     */
1825
    public function addNeverAllowedRegex(array $strings): self
1826
    {
1827
        if ($strings === []) {
9✔
1828
            return $this;
×
1829
        }
1830

1831
        // reset
1832
        $this->_cache_never_allowed_regex_string = '';
9✔
1833

1834
        $this->_never_allowed_regex = \array_merge(
9✔
1835
            $strings,
9✔
1836
            $this->_never_allowed_regex
9✔
1837
        );
5✔
1838

1839
        return $this;
9✔
1840
    }
1841

1842
    /**
1843
     * Remove some strings from the "_never_allowed_regex"-array.
1844
     *
1845
     * <p>
1846
     * <br />
1847
     * WARNING: Use this method only if you have a really good reason.
1848
     * </p>
1849
     *
1850
     * @param string[] $strings
1851
     *
1852
     * @return $this
1853
     */
1854
    public function removeNeverAllowedRegex(array $strings): self
1855
    {
1856
        if ($strings === []) {
18✔
1857
            return $this;
×
1858
        }
1859

1860
        // reset
1861
        $this->_cache_never_allowed_regex_string = '';
18✔
1862

1863
        $this->_never_allowed_regex = \array_diff(
18✔
1864
            $this->_never_allowed_regex,
18✔
1865
            \array_intersect($strings, $this->_never_allowed_regex)
18✔
1866
        );
10✔
1867

1868
        return $this;
18✔
1869
    }
1870

1871
    /**
1872
     * Add some strings to the "_never_allowed_on_events_afterwards"-array.
1873
     *
1874
     * @param string[] $strings
1875
     *
1876
     * @return $this
1877
     */
1878
    public function addNeverAllowedOnEventsAfterwards(array $strings): self
1879
    {
1880
        if ($strings === []) {
9✔
1881
            return $this;
×
1882
        }
1883

1884
        // reset
1885
        $this->_cache_evil_attributes_regex_string = '';
9✔
1886

1887
        $this->_never_allowed_on_events_afterwards = \array_merge(
9✔
1888
            $strings,
9✔
1889
            $this->_never_allowed_on_events_afterwards
9✔
1890
        );
5✔
1891

1892
        return $this;
9✔
1893
    }
1894

1895
    /**
1896
     * Add some strings to the "_never_allowed_str_afterwards"-array.
1897
     *
1898
     * @param string[] $strings
1899
     *
1900
     * @return $this
1901
     */
1902
    public function addNeverAllowedStrAfterwards(array $strings): self
1903
    {
1904
        if ($strings === []) {
9✔
1905
            return $this;
×
1906
        }
1907

1908
        $this->_never_allowed_str_afterwards = \array_merge(
9✔
1909
            $strings,
9✔
1910
            $this->_never_allowed_str_afterwards
9✔
1911
        );
5✔
1912

1913
        return $this;
9✔
1914
    }
1915

1916
    /**
1917
     * Add some strings to the "_do_not_close_html_tags"-array.
1918
     *
1919
     * @param string[] $strings
1920
     *
1921
     * @return $this
1922
     */
1923
    public function addDoNotCloseHtmlTags(array $strings): self
1924
    {
1925
        if ($strings === []) {
9✔
1926
            return $this;
×
1927
        }
1928

1929
        $this->_do_not_close_html_tags = \array_merge(
9✔
1930
            $strings,
9✔
1931
            $this->_do_not_close_html_tags
9✔
1932
        );
5✔
1933

1934
        return $this;
9✔
1935
    }
1936

1937
    /**
1938
     * Add some strings to the "_never_allowed_js_callback_regex"-array.
1939
     *
1940
     * @param string[] $strings
1941
     *
1942
     * @return $this
1943
     */
1944
    public function addNeverAllowedJsCallbackRegex(array $strings): self
1945
    {
1946
        if ($strings === []) {
9✔
1947
            return $this;
×
1948
        }
1949

1950
        $this->_never_allowed_js_callback_regex = \array_merge(
9✔
1951
            $strings,
9✔
1952
            $this->_never_allowed_js_callback_regex
9✔
1953
        );
5✔
1954

1955
        return $this;
9✔
1956
    }
1957
    
1958
    /**
1959
     * Add some strings to the "_never_allowed_call_strings"-array.
1960
     *
1961
     * @param string[] $strings
1962
     *
1963
     * @return $this
1964
     */
1965
    public function addNeverAllowedCallStrings(array $strings): self
1966
    {
1967
        if ($strings === []) {
9✔
1968
            return $this;
×
1969
        }
1970

1971
        $this->_never_allowed_call_strings = \array_merge(
9✔
1972
            $strings,
9✔
1973
            $this->_never_allowed_call_strings
9✔
1974
        );
5✔
1975

1976
        return $this;
9✔
1977
    }
1978

1979
    /**
1980
     * Remove some strings from the "_do_not_close_html_tags"-array.
1981
     *
1982
     * <p>
1983
     * <br />
1984
     * WARNING: Use this method only if you have a really good reason.
1985
     * </p>
1986
     *
1987
     * @param string[] $strings
1988
     *
1989
     * @return $this
1990
     */
1991
    public function removeDoNotCloseHtmlTags(array $strings): self
1992
    {
1993
        if ($strings === []) {
9✔
1994
            return $this;
×
1995
        }
1996

1997
        $this->_do_not_close_html_tags = \array_diff(
9✔
1998
            $this->_do_not_close_html_tags,
9✔
1999
            \array_intersect($strings, $this->_do_not_close_html_tags)
9✔
2000
        );
5✔
2001

2002
        return $this;
9✔
2003
    }
2004

2005
    /**
2006
     * Check if the "AntiXSS->xss_clean()"-method found an XSS attack in the last run.
2007
     *
2008
     * @return bool|null
2009
     *                   <p>Will return null if the "xss_clean()" wasn't running at all.</p>
2010
     */
2011
    public function isXssFound()
2012
    {
2013
        return $this->_xss_found;
738✔
2014
    }
2015

2016
    /**
2017
     * Remove some strings from the "_evil_attributes"-array.
2018
     *
2019
     * <p>
2020
     * <br />
2021
     * WARNING: Use this method only if you have a really good reason.
2022
     * </p>
2023
     *
2024
     * @param string[] $strings
2025
     *
2026
     * @return $this
2027
     */
2028
    public function removeEvilAttributes(array $strings): self
2029
    {
2030
        if ($strings === []) {
18✔
2031
            return $this;
×
2032
        }
2033

2034
        // reset
2035
        $this->_cache_evil_attributes_regex_string = '';
18✔
2036

2037
        $this->_evil_attributes_regex = \array_diff(
18✔
2038
            $this->_evil_attributes_regex,
18✔
2039
            \array_intersect($strings, $this->_evil_attributes_regex)
18✔
2040
        );
10✔
2041

2042
        return $this;
18✔
2043
    }
2044

2045
    /**
2046
     * Remove some strings from the "_evil_html_tags"-array.
2047
     *
2048
     * <p>
2049
     * <br />
2050
     * WARNING: Use this method only if you have a really good reason.
2051
     * </p>
2052
     *
2053
     * @param string[] $strings
2054
     *
2055
     * @return $this
2056
     */
2057
    public function removeEvilHtmlTags(array $strings): self
2058
    {
2059
        if ($strings === []) {
27✔
2060
            return $this;
×
2061
        }
2062

2063
        // reset
2064
        $this->_cache__evil_html_tags_str = '';
27✔
2065

2066
        $this->_evil_html_tags = \array_diff(
27✔
2067
            $this->_evil_html_tags,
27✔
2068
            \array_intersect($strings, $this->_evil_html_tags)
27✔
2069
        );
15✔
2070

2071
        return $this;
27✔
2072
    }
2073

2074
    /**
2075
     * Remove some strings from the "_never_allowed_on_events_afterwards"-array.
2076
     *
2077
     * <p>
2078
     * <br />
2079
     * WARNING: Use this method only if you have a really good reason.
2080
     * </p>
2081
     *
2082
     * @param string[] $strings
2083
     *
2084
     * @return $this
2085
     */
2086
    public function removeNeverAllowedOnEventsAfterwards(array $strings): self
2087
    {
2088
        if ($strings === []) {
9✔
2089
            return $this;
×
2090
        }
2091

2092
        // reset
2093
        $this->_cache_evil_attributes_regex_string = '';
9✔
2094

2095
        $this->_never_allowed_on_events_afterwards = \array_diff(
9✔
2096
            $this->_never_allowed_on_events_afterwards,
9✔
2097
            \array_intersect($strings, $this->_never_allowed_on_events_afterwards)
9✔
2098
        );
5✔
2099

2100
        return $this;
9✔
2101
    }
2102

2103
    /**
2104
     * Remove some strings from the "_never_allowed_str_afterwards"-array.
2105
     *
2106
     * <p>
2107
     * <br />
2108
     * WARNING: Use this method only if you have a really good reason.
2109
     * </p>
2110
     *
2111
     * @param string[] $strings
2112
     *
2113
     * @return $this
2114
     */
2115
    public function removeNeverAllowedStrAfterwards(array $strings): self
2116
    {
2117
        if ($strings === []) {
9✔
2118
            return $this;
×
2119
        }
2120

2121
        $this->_never_allowed_str_afterwards = \array_diff(
9✔
2122
            $this->_never_allowed_str_afterwards,
9✔
2123
            \array_intersect($strings, $this->_never_allowed_str_afterwards)
9✔
2124
        );
5✔
2125

2126
        return $this;
9✔
2127
    }
2128

2129
    /**
2130
     * Remove some strings from the "_never_allowed_call_strings"-array.
2131
     *
2132
     * <p>
2133
     * <br />
2134
     * WARNING: Use this method only if you have a really good reason.
2135
     * </p>
2136
     *
2137
     * @param string[] $strings
2138
     *
2139
     * @return $this
2140
     */
2141
    public function removeNeverAllowedCallStrings(array $strings): self
2142
    {
2143
        if ($strings === []) {
9✔
2144
            return $this;
×
2145
        }
2146

2147
        $this->_never_allowed_call_strings = \array_diff(
9✔
2148
            $this->_never_allowed_call_strings,
9✔
2149
            \array_intersect($strings, $this->_never_allowed_call_strings)
9✔
2150
        );
5✔
2151

2152
        return $this;
9✔
2153
    }
2154

2155
    /**
2156
     * Remove some strings from the "_never_allowed_js_callback_regex"-array.
2157
     *
2158
     * <p>
2159
     * <br />
2160
     * WARNING: Use this method only if you have a really good reason.
2161
     * </p>
2162
     *
2163
     * @param string[] $strings
2164
     *
2165
     * @return $this
2166
     */
2167
    public function removeNeverAllowedJsCallbackRegex(array $strings): self
2168
    {
2169
        if ($strings === []) {
9✔
2170
            return $this;
×
2171
        }
2172

2173
        $this->_never_allowed_js_callback_regex = \array_diff(
9✔
2174
            $this->_never_allowed_js_callback_regex,
9✔
2175
            \array_intersect($strings, $this->_never_allowed_js_callback_regex)
9✔
2176
        );
5✔
2177

2178
        return $this;
9✔
2179
    }
2180

2181
    /**
2182
     * Set the replacement-string for not allowed strings.
2183
     *
2184
     * @param string $string
2185
     *
2186
     * @return $this
2187
     */
2188
    public function setReplacement($string): self
2189
    {
2190
        $this->_replacement = (string) $string;
459✔
2191

2192
        $this->_initNeverAllowedStr();
459✔
2193
        $this->_initNeverAllowedRegex();
459✔
2194

2195
        return $this;
459✔
2196
    }
2197

2198
    /**
2199
     * Set the option to stripe 4-Byte chars.
2200
     *
2201
     * <p>
2202
     * <br />
2203
     * INFO: use it if your DB (MySQL) can't use "utf8mb4" -> preventing stored XSS-attacks
2204
     * </p>
2205
     *
2206
     * @param bool $bool
2207
     *
2208
     * @return $this
2209
     */
2210
    public function setStripe4byteChars($bool): self
2211
    {
2212
        $this->_stripe_4byte_chars = (bool) $bool;
9✔
2213

2214
        return $this;
9✔
2215
    }
2216

2217
    /**
2218
     * XSS Clean
2219
     *
2220
     * <p>
2221
     * <br />
2222
     * Sanitizes data so that "Cross Site Scripting" hacks can be
2223
     * prevented. This method does a fair amount of work but
2224
     * it is extremely thorough, designed to prevent even the
2225
     * most obscure XSS attempts. But keep in mind that nothing
2226
     * is ever 100% foolproof...
2227
     * </p>
2228
     *
2229
     * <p>
2230
     * <br />
2231
     * <strong>Note:</strong> Should only be used to deal with data upon submission.
2232
     *   It's not something that should be used for general
2233
     *   runtime processing.
2234
     * </p>
2235
     *
2236
     * @see http://channel.bitflux.ch/wiki/XSS_Prevention
2237
     *    Based in part on some code and ideas from Bitflux.
2238
     * @see http://ha.ckers.org/xss.html
2239
     *    To help develop this script I used this great list of
2240
     *    vulnerabilities along with a few other hacks I've
2241
     *    harvested from examining vulnerabilities in other programs.
2242
     *
2243
     * @param string|string[] $str
2244
     *                             <p>input data e.g. string or array of strings</p>
2245
     *
2246
     * @return string|string[]
2247
     *
2248
     * @template TXssCleanInput as string|string[]
2249
     * @phpstan-param TXssCleanInput $str
2250
     * @phpstan-return TXssCleanInput
2251
     */
2252
    public function xss_clean($str)
2253
    {
2254
        // reset
2255
        $this->_xss_found = null;
1,116✔
2256

2257
        // check for an array of strings
2258
        if (\is_array($str)) {
1,116✔
2259
            foreach ($str as &$value) {
36✔
2260
                /* @phpstan-ignore-next-line | _xss_found is maybe changed via "xss_clean" */
2261
                if ($this->_xss_found === true) {
36✔
2262
                    $alreadyFoundXss = true;
27✔
2263
                } else {
2264
                    $alreadyFoundXss = false;
36✔
2265
                }
2266
                
2267
                $value = $this->xss_clean($value);
36✔
2268

2269
                /* @phpstan-ignore-next-line | _xss_found is maybe changed via "xss_clean" */
2270
                if ($alreadyFoundXss === true) {
36✔
2271
                    $this->_xss_found = true;
29✔
2272
                }
2273
            }
2274

2275
            /** @var TXssCleanInput $str - hack for phpstan */
2276
            return $str;
36✔
2277
        }
2278

2279
        $old_str_backup = $str;
1,116✔
2280

2281
        // process
2282
        do {
2283
            $old_str = $str;
1,116✔
2284
            $str = $this->_do($str);
1,116✔
2285
        } while ($old_str !== $str);
1,116✔
2286

2287
        // keep the old value, if there wasn't any XSS attack
2288
        if ($this->_xss_found !== true) {
1,116✔
2289
            $str = $old_str_backup;
603✔
2290
        }
2291

2292
        return $str;
1,116✔
2293
    }
2294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc