Index: trunk/phase3/includes/WebRequest.php |
— | — | @@ -767,6 +767,69 @@ |
768 | 768 | } |
769 | 769 | |
770 | 770 | /** |
| 771 | + * Check if Internet Explorer will detect an incorrect cache extension in |
| 772 | + * PATH_INFO or QUERY_STRING. If the request can't be allowed, show an error |
| 773 | + * message or redirect to a safer URL. Returns true if the URL is OK, and |
| 774 | + * false if an error message has been shown and the request should be aborted. |
| 775 | + */ |
| 776 | + public function checkUrlExtension() { |
| 777 | + $query = isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : ''; |
| 778 | + if ( self::isUrlExtensionBad( $query ) ) { |
| 779 | + if ( !$this->wasPosted() ) { |
| 780 | + if ( $this->attemptExtensionSecurityRedirect() ) { |
| 781 | + return false; |
| 782 | + } |
| 783 | + } |
| 784 | + wfHttpError( 403, 'Forbidden', |
| 785 | + 'Invalid file extension found in the query string.' ); |
| 786 | + |
| 787 | + return false; |
| 788 | + } |
| 789 | + |
| 790 | + if ( $this->isPathInfoBad() ) { |
| 791 | + wfHttpError( 403, 'Forbidden', |
| 792 | + 'Invalid file extension found in PATH_INFO.' ); |
| 793 | + return false; |
| 794 | + } |
| 795 | + return true; |
| 796 | + } |
| 797 | + |
| 798 | + /** |
| 799 | + * Attempt to redirect to a URL with a QUERY_STRING that's not dangerous in |
| 800 | + * IE 6. Returns true if it was successful, false otherwise. |
| 801 | + */ |
| 802 | + protected function attemptExtensionSecurityRedirect() { |
| 803 | + $url = self::fixUrlForIE6( $this->getFullRequestURL() ); |
| 804 | + if ( $url === false ) { |
| 805 | + return false; |
| 806 | + } |
| 807 | + |
| 808 | + header( 'Location: ' . $url ); |
| 809 | + header( 'Content-Type: text/html' ); |
| 810 | + $encUrl = htmlspecialchars( $url ); |
| 811 | + echo <<<HTML |
| 812 | +<html> |
| 813 | +<head> |
| 814 | +<title>Security redirect</title> |
| 815 | +</head> |
| 816 | +<body> |
| 817 | +<h1>Security redirect</h1> |
| 818 | +<p> |
| 819 | +We can't serve non-HTML content from the URL you have requested, because |
| 820 | +Internet Explorer would interpret it as an incorrect and potentially dangerous |
| 821 | +content type.</p> |
| 822 | +<p>Instead, please use <a href="$encUrl">this URL</a>, which is the same as the URL you have requested, except that |
| 823 | +"&*" is appended. This prevents Internet Explorer from seeing a bogus file |
| 824 | +extension. |
| 825 | +</p> |
| 826 | +</body> |
| 827 | +</html> |
| 828 | +HTML; |
| 829 | + echo "\n"; |
| 830 | + return true; |
| 831 | + } |
| 832 | + |
| 833 | + /** |
771 | 834 | * Returns true if the PATH_INFO ends with an extension other than a script |
772 | 835 | * extension. This could confuse IE for scripts that send arbitrary data which |
773 | 836 | * is not HTML but may be detected as such. |
— | — | @@ -884,8 +947,18 @@ |
885 | 948 | if ( !isset( $_SERVER['QUERY_STRING'] ) ) { |
886 | 949 | return false; |
887 | 950 | } |
| 951 | + return self::isUrlExtensionBad( $_SERVER['QUERY_STRING'] ); |
| 952 | + } |
888 | 953 | |
889 | | - $extension = self::findIE6Extension( $_SERVER['QUERY_STRING'] ); |
| 954 | + /** |
| 955 | + * The same as WebRequest::isQueryStringBad() except as a static function. |
| 956 | + */ |
| 957 | + public static function isUrlExtensionBad( $query ) { |
| 958 | + if ( strval( $query ) === '' ) { |
| 959 | + return false; |
| 960 | + } |
| 961 | + |
| 962 | + $extension = self::findIE6Extension( $query ); |
890 | 963 | if ( strval( $extension ) === '' ) { |
891 | 964 | /* No extension or empty extension (false/'') */ |
892 | 965 | return false; |
— | — | @@ -902,6 +975,31 @@ |
903 | 976 | } |
904 | 977 | |
905 | 978 | /** |
| 979 | + * Returns a variant of $url which will pass isUrlExtensionBad() but has the |
| 980 | + * same GET parameters, or false if it can't figure one out. |
| 981 | + */ |
| 982 | + public static function fixUrlForIE6( $url ) { |
| 983 | + $questionPos = strpos( $url, '?' ); |
| 984 | + if ( $questionPos === false || $questionPos === strlen( $url ) - 1 ) { |
| 985 | + return $url; |
| 986 | + } |
| 987 | + |
| 988 | + $beforeQuery = substr( $url, 0, $questionPos + 1 ); |
| 989 | + $query = substr( $url, $questionPos + 1 ); |
| 990 | + // Multiple question marks cause problems. Encode the second and |
| 991 | + // subsequent question mark. |
| 992 | + $query = str_replace( '?', '%3E', $query ); |
| 993 | + // Append an invalid path character so that IE6 won't see the end of the |
| 994 | + // query string as an extension |
| 995 | + $query .= '&*'; |
| 996 | + if ( self::isUrlExtensionBad( $query ) ) { |
| 997 | + // Avoid a redirect loop |
| 998 | + return false; |
| 999 | + } |
| 1000 | + return $beforeQuery . $query; |
| 1001 | + } |
| 1002 | + |
| 1003 | + /** |
906 | 1004 | * Parse the Accept-Language header sent by the client into an array |
907 | 1005 | * @return array( languageCode => q-value ) sorted by q-value in descending order |
908 | 1006 | * May contain the "language" '*', which applies to languages other than those explicitly listed. |
Index: trunk/phase3/includes/RawPage.php |
— | — | @@ -118,22 +118,8 @@ |
119 | 119 | function view() { |
120 | 120 | global $wgOut, $wgRequest; |
121 | 121 | |
122 | | - if( $wgRequest->isPathInfoBad() ) { |
123 | | - # Internet Explorer will ignore the Content-Type header if it |
124 | | - # thinks it sees a file extension it recognizes. Make sure that |
125 | | - # all raw requests are done through the script node, which will |
126 | | - # have eg '.php' and should remain safe. |
127 | | - # |
128 | | - # We used to redirect to a canonical-form URL as a general |
129 | | - # backwards-compatibility / good-citizen nice thing. However |
130 | | - # a lot of servers are set up in buggy ways, resulting in |
131 | | - # redirect loops which hang the browser until the CSS load |
132 | | - # times out. |
133 | | - # |
134 | | - # Just return a 403 Forbidden and get it over with. |
135 | | - wfHttpError( 403, 'Forbidden', |
136 | | - 'Invalid file extension found in PATH_INFO or QUERY_STRING. ' . |
137 | | - 'Raw pages must be accessed through the primary script entry point.' ); |
| 122 | + if( !$wgRequest->checkUrlExtension() ) { |
| 123 | + $wgOut->disable(); |
138 | 124 | return; |
139 | 125 | } |
140 | 126 | |
Index: trunk/phase3/api.php |
— | — | @@ -61,18 +61,7 @@ |
62 | 62 | $starttime = microtime( true ); |
63 | 63 | |
64 | 64 | // URL safety checks |
65 | | -// |
66 | | -// See RawPage.php for details; summary is that MSIE can override the |
67 | | -// Content-Type if it sees a recognized extension on the URL, such as |
68 | | -// might be appended via PATH_INFO after 'api.php'. |
69 | | -// |
70 | | -// Some data formats can end up containing unfiltered user-provided data |
71 | | -// which will end up triggering HTML detection and execution, hence |
72 | | -// XSS injection and all that entails. |
73 | | -// |
74 | | -if ( $wgRequest->isPathInfoBad() ) { |
75 | | - wfHttpError( 403, 'Forbidden', |
76 | | - 'Invalid file extension found in PATH_INFO or QUERY_STRING.' ); |
| 65 | +if ( !$wgRequest->checkUrlExtension() ) { |
77 | 66 | return; |
78 | 67 | } |
79 | 68 | |
Index: trunk/phase3/load.php |
— | — | @@ -45,17 +45,7 @@ |
46 | 46 | wfProfileIn( 'load.php' ); |
47 | 47 | |
48 | 48 | // URL safety checks |
49 | | -// |
50 | | -// See RawPage.php for details; summary is that MSIE can override the |
51 | | -// Content-Type if it sees a recognized extension on the URL, such as |
52 | | -// might be appended via PATH_INFO after 'load.php'. |
53 | | -// |
54 | | -// Some resources can contain HTML-like strings (e.g. in messages) |
55 | | -// which will end up triggering HTML detection and execution. |
56 | | -// |
57 | | -if ( $wgRequest->isPathInfoBad() ) { |
58 | | - wfHttpError( 403, 'Forbidden', |
59 | | - 'Invalid file extension found in PATH_INFO or QUERY_STRING.' ); |
| 49 | +if ( !$wgRequest->checkUrlExtension() ) { |
60 | 50 | return; |
61 | 51 | } |
62 | 52 | |