noxss   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 84.78%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 0
dl 0
loc 144
ccs 39
cts 46
cp 0.8478
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A ignore() 0 4 1
B detect() 0 20 7
B _gatherXSSInput() 0 20 8
A prevent() 0 19 4
B _checkForProblems() 0 20 6
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 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')
164
    {
165
        self::$ignoreList[$method][$name] = 1;
166
    }
167
}
168