1 | <?php |
||
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 | 2 | public static function detect() |
|
68 | { |
||
69 | 2 | foreach ([ 'GET' => $_GET, 'POST' => $_POST, 'COOKIE' => $_COOKIE ] as $method => $inputs) { |
|
70 | 2 | if (is_array( $inputs )) { |
|
71 | 2 | self::_gatherXSSInput( $inputs, $method ); |
|
72 | } |
||
73 | } |
||
74 | 2 | foreach (self::$xssHeaders as $header) { |
|
75 | 2 | if (array_key_exists( $header, $_SERVER )) { |
|
76 | self::_gatherXSSInput( $_SERVER[$header], 'SERVER' ); |
||
77 | } |
||
78 | } |
||
79 | |||
80 | 2 | 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 | 2 | ob_start(); |
|
84 | 2 | self::$potentialXSS = true; |
|
85 | } |
||
86 | 2 | } |
|
87 | |||
88 | 2 | private static function _gatherXSSInput($input, $method, $name = null) |
|
89 | { |
||
90 | 2 | if (is_array( $input )) { |
|
91 | 2 | foreach ($input as $key => $value) { |
|
92 | 2 | if (!isset($name)) { |
|
93 | 2 | self::_gatherXSSInput( $value, $method, $key ); |
|
94 | } else { |
||
95 | self::_gatherXSSInput( $value, $method, $name ); |
||
96 | } |
||
97 | } |
||
98 | } else { |
||
99 | 2 | $input = (string) $input; |
|
100 | 2 | if (( !array_key_exists( $method, self::$ignoreList ) || !array_key_exists( $name, self::$ignoreList[$method] ) ) |
|
101 | 2 | && ( strlen( $input ) > self::$minimumLength ) |
|
102 | 2 | && preg_match( self::$reXSS, $input, $matches)) |
|
103 | { |
||
104 | 2 | self::$xss[ $method ][ strlen($input) ][] = $input; |
|
105 | } |
||
106 | } |
||
107 | 2 | } |
|
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 | 2 | public static function prevent($f = null) |
|
123 | { |
||
124 | 2 | if (self::$potentialXSS) { |
|
125 | 2 | self::$output = ob_get_contents(); |
|
126 | 2 | ob_end_clean(); |
|
127 | |||
128 | 2 | $xssDetected = self::_checkForProblems(); |
|
129 | |||
130 | 2 | if ($xssDetected) { |
|
131 | 2 | if (is_callable($f)) { |
|
132 | 2 | $f( self::$output ); |
|
133 | } else { |
||
134 | 2 | header( 'HTTP/1.1 400 Bad Request' ); |
|
135 | } |
||
136 | } else { |
||
137 | echo self::$output; |
||
138 | } |
||
139 | } |
||
140 | 2 | } |
|
141 | |||
142 | 2 | 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 | 2 | foreach (self::$xss as $inputs) { |
|
147 | 2 | krsort( $inputs, SORT_NUMERIC ); |
|
148 | 2 | foreach ($inputs as $values) { |
|
149 | 2 | if (is_array($values)) { |
|
150 | 2 | foreach ($values as $value) { |
|
151 | 2 | if (false !== strpos( self::$output, $value)) { |
|
152 | // One of the potential XSS attack inputs has been found _unchanged_ in the output |
||
153 | 2 | return true; |
|
154 | } |
||
155 | } |
||
156 | } |
||
157 | } |
||
158 | } |
||
159 | |||
160 | return false; |
||
161 | } |
||
162 | |||
163 | public static function ignore($name, $method = 'GET') |
||
167 | } |
||
168 |