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