Completed
Push — master ( cee824...b1d97d )
by Robbert
06:12 queued 02:02
created

noxss::detect()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 7
Bugs 1 Features 2
Metric Value
c 7
b 1
f 2
dl 0
loc 20
ccs 11
cts 11
cp 1
rs 8.2222
cc 7
eloc 10
nc 18
nop 0
crap 7
1
<?php
2
/*
3
 * This file is part of the Ariadne Component Library.
4
 *
5
 * (c) Muze <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace arc;
11
12
/**
13
 *	noxss is an XSS attack detection and prevention class. It contains two methods, detect and prevent.
14
 *  The detect() method must be called at the start of handling any request, e.g. in your front loader / router
15
 *  The prevent() method must be called at the end of handling any request.
16
 *
17
 *	Usage:
18
 *    <?php
19
 *        \arc\noxss::detect();
20
 *        \\ handle request normally
21
 *        \arc\noxss::prevent();
22
 *    ?>
23
 */
24
final class noxss
25
{
26
    /**
27
     * @var (bool) A flag to indicate if there might be an XSS attack going on
28
     */
29
    public static $potentialXSS = false;
30
31
    /**
32
     * @var (array) A container for inputs potentially containing XSS attacks
33
     */
34
    public static $xss;
35
36
    /**
37
     * @var (string) buffered output caught by prevent.
38
     */
39
    public static $output;
40
41
    /**
42
     * @var (string) Regular expression that matches any input containing quotes, tag start or end delimiters or &.
43
     * I don't know any XSS attack that doesn't require at least one of these characters.
44
     */
45
    public static $reXSS = '/[\'"<>&]/';
46
47
    /**
48
     * @var (array) A list of _SERVER variables sent by client header and thus potential attack vectors, can be set
49
     * by user when needed / used.
50
     */
51
    public static $xssHeaders = [ 'PHP_AUTH_USER', 'PHP_AUTH_PW' ];
52
53
    /**
54
     * @var (int) Minimum length of an input to qualify as a potential XSS attack.
55
     */
56
    public static $minimumLength = 10;
57
58
    /**
59
     * @var (array) A list of inputs to ignore, keyed to the input method - GET, POST, COOKIE, SERVER
60
     */
61
    public static $ignoreList = [];
62
63
    /**
64
     * This method checks all user inputs ( get/post/cookie variables, client sent headers ) for potential XSS attacks
65
     * If found it flags these and sets self::$potentialXSS to true and starts an output buffer
66
     */
67 1
    public static function detect()
68
    {
69 1
        foreach ([ 'GET' => $_GET, 'POST' => $_POST, 'COOKIE' => $_COOKIE ] as $method => $inputs) {
70 1
            if (is_array( $inputs )) {
71 1
                self::_gatherXSSInput( $inputs, $method );
72
            }
73
        }
74 1
        foreach (self::$xssHeaders as $header) {
75 1
            if (array_key_exists( $header, $_SERVER )) {
76 1
                self::_gatherXSSInput( $_SERVER[$header], 'SERVER' );
77
            }
78
        }
79
80 1
        if (!self::$potentialXSS && count( self::$xss )) {
81
            // An input with problematic tokens has been spotted, start the output buffer once
82
            // to check the output for an occurance of that input _unchanged_
83 1
            ob_start();
84 1
            self::$potentialXSS = true;
85
        }
86 1
    }
87
88 1
    private static function _gatherXSSInput($input, $method, $name = null)
89
    {
90 1
        if (is_array( $input )) {
91 1
            foreach ($input as $key => $value) {
92 1
                if (!isset($name)) {
93 1
                    self::_gatherXSSInput( $value, $method, $key );
94
                } else {
95 1
                    self::_gatherXSSInput( $value, $method, $name );
96
                }
97
            }
98
        } else {
99 1
            $input = (string) $input;
100 1
            if (( !array_key_exists( $method, self::$ignoreList ) || !array_key_exists( $name, self::$ignoreList[$method] ) )
101 1
                && ( strlen( $input ) > self::$minimumLength )
102 1
                && preg_match( self::$reXSS, $input, $matches))
103
            {
104 1
                self::$xss[ $method ][ strlen($input) ][] = $input;
105
            }
106
        }
107 1
    }
108
109
    /**
110
     * This method checks if self::$potentialXSS to see if an XSS attack might be going on. If so
111
     * the output buffer is ended and the output content retrieved. All inputs flagged as potential XSS attacks
112
     * are checked to see if any of these is in the output content _in_unchanged_form_ !
113
     * If so, there is a vulnerability to XSS which is being exploited ( or at least triggered ) and the only
114
     * safe option is to not sent the output but sent a 400 Bad Request header instead.
115
     * This method doesn't actually send this header but it does throw an exception allowing you to handle it
116
     * any way you see fit
117
     *
118
     * @param callable $f (optional) A method to call when a potential xss attack is detected. Takes one argument: the output
119
     *                    generated by this request so far. If not set prevent() will just sent a 400 Bad Request header if a potential xss attack
120
     *                    is detected.
121
     */
122 1
    public static function prevent($f = null)
123
    {
124 1
        if (self::$potentialXSS) {
125 1
            self::$output = ob_get_contents();
126 1
            ob_end_clean();
127
128 1
            $xssDetected = self::_checkForProblems();
129
130 1
            if ($xssDetected) {
131 1
                if (is_callable($f)) {
132 1
                    $f( self::$output );
133
                } else {
134 1
                    header( 'HTTP/1.1 400 Bad Request' );
135
                }
136
            } else {
137
                echo self::$output;
138
            }
139
        }
140 1
    }
141
142 1
    private static function _checkForProblems()
143
    {
144
        // sort by length of string so longer strings are matched first
145
        // key is set to the length of the string by detect()
146 1
        foreach (self::$xss as $inputs) {
147 1
            krsort( $inputs, SORT_NUMERIC );
148 1
            foreach ($inputs as $values) {
149 1
                if (is_array($values)) {
150 1
                    foreach ($values as $value) {
151 1
                        if (false !== strpos( self::$output, $value)) {
152
                            // One of the potential XSS attack inputs has been found _unchanged_ in the output
153 1
                            return true;
154
                        }
155
                    }
156
                }
157
            }
158
        }
159
160
        return false;
161
    }
162
163
    public static function ignore($name, $method = 'GET')
164
    {
165
        self::$ignoreList[$method][$name] = 1;
166
    }
167
}
168