]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/libs/IEUrlExtension.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / libs / IEUrlExtension.php
1 <?php
2
3 /**
4  * Internet Explorer derives a cache filename from a URL, and then in certain 
5  * circumstances, uses the extension of the resulting file to determine the 
6  * content type of the data, ignoring the Content-Type header. 
7  *
8  * This can be a problem, especially when non-HTML content is sent by MediaWiki,
9  * and Internet Explorer interprets it as HTML, exposing an XSS vulnerability.
10  *
11  * Usually the script filename (e.g. api.php) is present in the URL, and this 
12  * makes Internet Explorer think the extension is a harmless script extension.
13  * But Internet Explorer 6 and earlier allows the script extension to be 
14  * obscured by encoding the dot as "%2E". 
15  *
16  * This class contains functions which help in detecting and dealing with this 
17  * situation.
18  *
19  * Checking the URL for a bad extension is somewhat complicated due to the fact 
20  * that CGI doesn't provide a standard method to determine the URL. Instead it
21  * is necessary to pass a subset of $_SERVER variables, which we then attempt 
22  * to use to guess parts of the URL.
23  */
24 class IEUrlExtension {
25         /**
26          * Check a subset of $_SERVER (or the whole of $_SERVER if you like)
27          * to see if it indicates that the request was sent with a bad file 
28          * extension. Returns true if the request should be denied or modified, 
29          * false otherwise. The relevant $_SERVER elements are:
30          *
31          *   - SERVER_SOFTWARE
32          *   - REQUEST_URI
33          *   - QUERY_STRING
34          *   - PATH_INFO
35          *
36          * If the a variable is unset in $_SERVER, it should be unset in $vars.
37          *
38          * @param $vars A subset of $_SERVER.
39          * @param $extWhitelist Extensions which are allowed, assumed harmless.
40          */
41         public static function areServerVarsBad( $vars, $extWhitelist = array() ) {
42                 // Check QUERY_STRING or REQUEST_URI
43                 if ( isset( $vars['SERVER_SOFTWARE'] )
44                         && isset( $vars['REQUEST_URI'] )
45                         && self::haveUndecodedRequestUri( $vars['SERVER_SOFTWARE'] ) )
46                 {
47                         $urlPart = $vars['REQUEST_URI'];
48                 } elseif ( isset( $vars['QUERY_STRING'] ) ) {
49                         $urlPart = $vars['QUERY_STRING'];
50                 } else {
51                         $urlPart = '';
52                 }
53
54                 if ( self::isUrlExtensionBad( $urlPart, $extWhitelist ) ) {
55                         return true;
56                 }
57
58                 // Some servers have PATH_INFO but not REQUEST_URI, so we check both 
59                 // to be on the safe side.
60                 if ( isset( $vars['PATH_INFO'] )
61                         && self::isUrlExtensionBad( $vars['PATH_INFO'], $extWhitelist ) )
62                 {
63                         return true;
64                 }
65
66                 // All checks passed
67                 return false;
68         }
69
70         /**
71          * Given a right-hand portion of a URL, determine whether IE would detect
72          * a potentially harmful file extension.
73          *
74          * @param $urlPart The right-hand portion of a URL
75          * @param $extWhitelist An array of file extensions which may occur in this
76          *    URL, and which should be allowed.
77          * @return bool
78          */
79         public static function isUrlExtensionBad( $urlPart, $extWhitelist = array() ) {
80                 if ( strval( $urlPart ) === '' ) {
81                         return false;
82                 }
83
84                 $extension = self::findIE6Extension( $urlPart );
85                 if ( strval( $extension ) === '' ) {
86                         // No extension or empty extension
87                         return false;
88                 }
89
90                 if ( in_array( $extension, array( 'php', 'php5' ) ) ) {
91                         // Script extension, OK
92                         return false;
93                 }
94                 if ( in_array( $extension, $extWhitelist ) ) {
95                         // Whitelisted extension
96                         return false;
97                 }
98
99                 if ( !preg_match( '/^[a-zA-Z0-9_-]+$/', $extension ) ) {
100                         // Non-alphanumeric extension, unlikely to be registered. 
101                         //
102                         // The regex above is known to match all registered file extensions
103                         // in a default Windows XP installation. It's important to allow 
104                         // extensions with ampersands and percent signs, since that reduces
105                         // the number of false positives substantially.
106                         return false;
107                 }
108
109                 // Possibly bad extension
110                 return true;
111         }
112
113         /**
114          * Returns a variant of $url which will pass isUrlExtensionBad() but has the 
115          * same GET parameters, or false if it can't figure one out.
116          */
117         public static function fixUrlForIE6( $url, $extWhitelist = array() ) {
118                 $questionPos = strpos( $url, '?' );
119                 if ( $questionPos === false ) {
120                         $beforeQuery = $url . '?';
121                         $query = '';
122                 } elseif ( $questionPos === strlen( $url ) - 1 ) {
123                         $beforeQuery = $url;
124                         $query = '';
125                 } else {
126                         $beforeQuery = substr( $url, 0, $questionPos + 1 );
127                         $query = substr( $url, $questionPos + 1 );
128                 }
129
130                 // Multiple question marks cause problems. Encode the second and 
131                 // subsequent question mark.
132                 $query = str_replace( '?', '%3E', $query );
133                 // Append an invalid path character so that IE6 won't see the end of the
134                 // query string as an extension
135                 $query .= '&*';
136                 // Put the URL back together
137                 $url = $beforeQuery . $query;
138                 if ( self::isUrlExtensionBad( $url, $extWhitelist ) ) {
139                         // Avoid a redirect loop
140                         return false;
141                 }
142                 return $url;
143         }
144
145         /**
146          * Determine what extension IE6 will infer from a certain query string.
147          * If the URL has an extension before the question mark, IE6 will use
148          * that and ignore the query string, but per the comment at
149          * isPathInfoBad() we don't have a reliable way to determine the URL,
150          * so isPathInfoBad() just passes in the query string for $url.
151          * All entry points have safe extensions (php, php5) anyway, so
152          * checking the query string is possibly overly paranoid but never
153          * insecure.
154          *
155          * The criteria for finding an extension are as follows:
156          * - a possible extension is a dot followed by one or more characters not 
157          *   in <>\"/:|?.#
158          * - if we find a possible extension followed by the end of the string or 
159          *   a #, that's our extension
160          * - if we find a possible extension followed by a ?, that's our extension
161          *    - UNLESS it's exe, dll or cgi, in which case we ignore it and continue 
162          *      searching for another possible extension
163          * - if we find a possible extension followed by a dot or another illegal 
164          *   character, we ignore it and continue searching
165          * 
166          * @param $url string URL
167          * @return mixed Detected extension (string), or false if none found
168          */
169         public static function findIE6Extension( $url ) {
170                 $pos = 0;
171                 $hashPos = strpos( $url, '#' );
172                 if ( $hashPos !== false ) {
173                         $urlLength = $hashPos;
174                 } else {
175                         $urlLength = strlen( $url );
176                 }
177                 $remainingLength = $urlLength;
178                 while ( $remainingLength > 0 ) {
179                         // Skip ahead to the next dot
180                         $pos += strcspn( $url, '.', $pos, $remainingLength );
181                         if ( $pos >= $urlLength ) {
182                                 // End of string, we're done
183                                 return false;
184                         }
185                         
186                         // We found a dot. Skip past it
187                         $pos++;
188                         $remainingLength = $urlLength - $pos;
189
190                         // Check for illegal characters in our prospective extension,
191                         // or for another dot
192                         $nextPos = $pos + strcspn( $url, "<>\\\"/:|?*.", $pos, $remainingLength );
193                         if ( $nextPos >= $urlLength ) {
194                                 // No illegal character or next dot
195                                 // We have our extension
196                                 return substr( $url, $pos, $urlLength - $pos );
197                         }
198                         if ( $url[$nextPos] === '?' ) {
199                                 // We've found a legal extension followed by a question mark
200                                 // If the extension is NOT exe, dll or cgi, return it
201                                 $extension = substr( $url, $pos, $nextPos - $pos );
202                                 if ( strcasecmp( $extension, 'exe' ) && strcasecmp( $extension, 'dll' ) &&
203                                         strcasecmp( $extension, 'cgi' ) )
204                                 {
205                                         return $extension;
206                                 }
207                                 // Else continue looking
208                         }
209                         // We found an illegal character or another dot
210                         // Skip to that character and continue the loop
211                         $pos = $nextPos + 1;
212                         $remainingLength = $urlLength - $pos;
213                 }
214                 return false;
215         }
216
217         /**
218          * When passed the value of $_SERVER['SERVER_SOFTWARE'], this function
219          * returns true if that server is known to have a REQUEST_URI variable
220          * with %2E not decoded to ".". On such a server, it is possible to detect
221          * whether the script filename has been obscured.
222          *
223          * The function returns false if the server is not known to have this 
224          * behaviour. Microsoft IIS in particular is known to decode escaped script
225          * filenames.
226          *
227          * SERVER_SOFTWARE typically contains either a plain string such as "Zeus",
228          * or a specification in the style of a User-Agent header, such as 
229          * "Apache/1.3.34 (Unix) mod_ssl/2.8.25 OpenSSL/0.9.8a PHP/4.4.2"
230          *
231          * @param $serverSoftware
232          * @return bool
233          *
234          */
235         public static function haveUndecodedRequestUri( $serverSoftware ) {
236                 static $whitelist = array(
237                         'Apache', 
238                         'Zeus', 
239                         'LiteSpeed' );
240                 if ( preg_match( '/^(.*?)($|\/| )/', $serverSoftware, $m ) ) {
241                         return in_array( $m[1], $whitelist );
242                 } else {
243                         return false;
244                 }
245         }
246
247 }