Merge "Remove JS for FancyCaptcha adjustment"
[lhc/web/wiklou.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 = array() ) {
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 = array() ) {
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, array( '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 //
123 // The regex above is known to match all registered file extensions
124 // in a default Windows XP installation. It's important to allow
125 // extensions with ampersands and percent signs, since that reduces
126 // the number of false positives substantially.
127 return false;
128 }
129
130 // Possibly bad extension
131 return true;
132 }
133
134 /**
135 * Returns a variant of $url which will pass isUrlExtensionBad() but has the
136 * same GET parameters, or false if it can't figure one out.
137 * @param $url
138 * @param $extWhitelist array
139 * @return bool|string
140 */
141 public static function fixUrlForIE6( $url, $extWhitelist = array() ) {
142 $questionPos = strpos( $url, '?' );
143 if ( $questionPos === false ) {
144 $beforeQuery = $url . '?';
145 $query = '';
146 } elseif ( $questionPos === strlen( $url ) - 1 ) {
147 $beforeQuery = $url;
148 $query = '';
149 } else {
150 $beforeQuery = substr( $url, 0, $questionPos + 1 );
151 $query = substr( $url, $questionPos + 1 );
152 }
153
154 // Multiple question marks cause problems. Encode the second and
155 // subsequent question mark.
156 $query = str_replace( '?', '%3E', $query );
157 // Append an invalid path character so that IE6 won't see the end of the
158 // query string as an extension
159 $query .= '&*';
160 // Put the URL back together
161 $url = $beforeQuery . $query;
162 if ( self::isUrlExtensionBad( $url, $extWhitelist ) ) {
163 // Avoid a redirect loop
164 return false;
165 }
166 return $url;
167 }
168
169 /**
170 * Determine what extension IE6 will infer from a certain query string.
171 * If the URL has an extension before the question mark, IE6 will use
172 * that and ignore the query string, but per the comment at
173 * isPathInfoBad() we don't have a reliable way to determine the URL,
174 * so isPathInfoBad() just passes in the query string for $url.
175 * All entry points have safe extensions (php, php5) anyway, so
176 * checking the query string is possibly overly paranoid but never
177 * insecure.
178 *
179 * The criteria for finding an extension are as follows:
180 * - a possible extension is a dot followed by one or more characters not
181 * in <>\"/:|?.#
182 * - if we find a possible extension followed by the end of the string or
183 * a #, that's our extension
184 * - if we find a possible extension followed by a ?, that's our extension
185 * - UNLESS it's exe, dll or cgi, in which case we ignore it and continue
186 * searching for another possible extension
187 * - if we find a possible extension followed by a dot or another illegal
188 * character, we ignore it and continue searching
189 *
190 * @param string $url URL
191 * @return mixed Detected extension (string), or false if none found
192 */
193 public static function findIE6Extension( $url ) {
194 $pos = 0;
195 $hashPos = strpos( $url, '#' );
196 if ( $hashPos !== false ) {
197 $urlLength = $hashPos;
198 } else {
199 $urlLength = strlen( $url );
200 }
201 $remainingLength = $urlLength;
202 while ( $remainingLength > 0 ) {
203 // Skip ahead to the next dot
204 $pos += strcspn( $url, '.', $pos, $remainingLength );
205 if ( $pos >= $urlLength ) {
206 // End of string, we're done
207 return false;
208 }
209
210 // We found a dot. Skip past it
211 $pos++;
212 $remainingLength = $urlLength - $pos;
213
214 // Check for illegal characters in our prospective extension,
215 // or for another dot
216 $nextPos = $pos + strcspn( $url, "<>\\\"/:|?*.", $pos, $remainingLength );
217 if ( $nextPos >= $urlLength ) {
218 // No illegal character or next dot
219 // We have our extension
220 return substr( $url, $pos, $urlLength - $pos );
221 }
222 if ( $url[$nextPos] === '?' ) {
223 // We've found a legal extension followed by a question mark
224 // If the extension is NOT exe, dll or cgi, return it
225 $extension = substr( $url, $pos, $nextPos - $pos );
226 if ( strcasecmp( $extension, 'exe' ) && strcasecmp( $extension, 'dll' ) &&
227 strcasecmp( $extension, 'cgi' ) )
228 {
229 return $extension;
230 }
231 // Else continue looking
232 }
233 // We found an illegal character or another dot
234 // Skip to that character and continue the loop
235 $pos = $nextPos;
236 $remainingLength = $urlLength - $pos;
237 }
238 return false;
239 }
240
241 /**
242 * When passed the value of $_SERVER['SERVER_SOFTWARE'], this function
243 * returns true if that server is known to have a REQUEST_URI variable
244 * with %2E not decoded to ".". On such a server, it is possible to detect
245 * whether the script filename has been obscured.
246 *
247 * The function returns false if the server is not known to have this
248 * behavior. Microsoft IIS in particular is known to decode escaped script
249 * filenames.
250 *
251 * SERVER_SOFTWARE typically contains either a plain string such as "Zeus",
252 * or a specification in the style of a User-Agent header, such as
253 * "Apache/1.3.34 (Unix) mod_ssl/2.8.25 OpenSSL/0.9.8a PHP/4.4.2"
254 *
255 * @param $serverSoftware
256 * @return bool
257 *
258 */
259 public static function haveUndecodedRequestUri( $serverSoftware ) {
260 static $whitelist = array(
261 'Apache',
262 'Zeus',
263 'LiteSpeed' );
264 if ( preg_match( '/^(.*?)($|\/| )/', $serverSoftware, $m ) ) {
265 return in_array( $m[1], $whitelist );
266 } else {
267 return false;
268 }
269 }
270
271 }