Index: trunk/phase3/tests/phpunit/includes/FindIE6ExtensionTest.php |
— | — | @@ -1,13 +1,13 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | 4 | /** |
5 | | - * Tests for WebRequest::findIE6Extension |
| 5 | + * Tests for IEUrlExtension::findIE6Extension |
6 | 6 | */ |
7 | | -class FindIE6ExtensionTest extends MediaWikiTestCase { |
| 7 | +class IEUrlExtensionTest extends MediaWikiTestCase { |
8 | 8 | function testSimple() { |
9 | 9 | $this->assertEquals( |
10 | 10 | 'y', |
11 | | - WebRequest::findIE6Extension( 'x.y' ), |
| 11 | + IEUrlExtension::findIE6Extension( 'x.y' ), |
12 | 12 | 'Simple extension' |
13 | 13 | ); |
14 | 14 | } |
— | — | @@ -15,7 +15,7 @@ |
16 | 16 | function testSimpleNoExt() { |
17 | 17 | $this->assertEquals( |
18 | 18 | '', |
19 | | - WebRequest::findIE6Extension( 'x' ), |
| 19 | + IEUrlExtension::findIE6Extension( 'x' ), |
20 | 20 | 'No extension' |
21 | 21 | ); |
22 | 22 | } |
— | — | @@ -23,7 +23,7 @@ |
24 | 24 | function testEmpty() { |
25 | 25 | $this->assertEquals( |
26 | 26 | '', |
27 | | - WebRequest::findIE6Extension( '' ), |
| 27 | + IEUrlExtension::findIE6Extension( '' ), |
28 | 28 | 'Empty string' |
29 | 29 | ); |
30 | 30 | } |
— | — | @@ -31,7 +31,7 @@ |
32 | 32 | function testQuestionMark() { |
33 | 33 | $this->assertEquals( |
34 | 34 | '', |
35 | | - WebRequest::findIE6Extension( '?' ), |
| 35 | + IEUrlExtension::findIE6Extension( '?' ), |
36 | 36 | 'Question mark only' |
37 | 37 | ); |
38 | 38 | } |
— | — | @@ -39,7 +39,7 @@ |
40 | 40 | function testExtQuestionMark() { |
41 | 41 | $this->assertEquals( |
42 | 42 | 'x', |
43 | | - WebRequest::findIE6Extension( '.x?' ), |
| 43 | + IEUrlExtension::findIE6Extension( '.x?' ), |
44 | 44 | 'Extension then question mark' |
45 | 45 | ); |
46 | 46 | } |
— | — | @@ -47,7 +47,7 @@ |
48 | 48 | function testQuestionMarkExt() { |
49 | 49 | $this->assertEquals( |
50 | 50 | 'x', |
51 | | - WebRequest::findIE6Extension( '?.x' ), |
| 51 | + IEUrlExtension::findIE6Extension( '?.x' ), |
52 | 52 | 'Question mark then extension' |
53 | 53 | ); |
54 | 54 | } |
— | — | @@ -55,7 +55,7 @@ |
56 | 56 | function testInvalidChar() { |
57 | 57 | $this->assertEquals( |
58 | 58 | '', |
59 | | - WebRequest::findIE6Extension( '.x*' ), |
| 59 | + IEUrlExtension::findIE6Extension( '.x*' ), |
60 | 60 | 'Extension with invalid character' |
61 | 61 | ); |
62 | 62 | } |
— | — | @@ -63,7 +63,7 @@ |
64 | 64 | function testInvalidCharThenExtension() { |
65 | 65 | $this->assertEquals( |
66 | 66 | 'x', |
67 | | - WebRequest::findIE6Extension( '*.x' ), |
| 67 | + IEUrlExtension::findIE6Extension( '*.x' ), |
68 | 68 | 'Invalid character followed by an extension' |
69 | 69 | ); |
70 | 70 | } |
— | — | @@ -71,7 +71,7 @@ |
72 | 72 | function testMultipleQuestionMarks() { |
73 | 73 | $this->assertEquals( |
74 | 74 | 'c', |
75 | | - WebRequest::findIE6Extension( 'a?b?.c?.d?e?f' ), |
| 75 | + IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ), |
76 | 76 | 'Multiple question marks' |
77 | 77 | ); |
78 | 78 | } |
— | — | @@ -79,7 +79,7 @@ |
80 | 80 | function testExeException() { |
81 | 81 | $this->assertEquals( |
82 | 82 | 'd', |
83 | | - WebRequest::findIE6Extension( 'a?b?.exe?.d?.e' ), |
| 83 | + IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ), |
84 | 84 | '.exe exception' |
85 | 85 | ); |
86 | 86 | } |
— | — | @@ -87,7 +87,7 @@ |
88 | 88 | function testExeException2() { |
89 | 89 | $this->assertEquals( |
90 | 90 | 'exe', |
91 | | - WebRequest::findIE6Extension( 'a?b?.exe' ), |
| 91 | + IEUrlExtension::findIE6Extension( 'a?b?.exe' ), |
92 | 92 | '.exe exception 2' |
93 | 93 | ); |
94 | 94 | } |
— | — | @@ -95,7 +95,7 @@ |
96 | 96 | function testHash() { |
97 | 97 | $this->assertEquals( |
98 | 98 | '', |
99 | | - WebRequest::findIE6Extension( 'a#b.c' ), |
| 99 | + IEUrlExtension::findIE6Extension( 'a#b.c' ), |
100 | 100 | 'Hash character preceding extension' |
101 | 101 | ); |
102 | 102 | } |
— | — | @@ -103,7 +103,7 @@ |
104 | 104 | function testHash2() { |
105 | 105 | $this->assertEquals( |
106 | 106 | '', |
107 | | - WebRequest::findIE6Extension( 'a?#b.c' ), |
| 107 | + IEUrlExtension::findIE6Extension( 'a?#b.c' ), |
108 | 108 | 'Hash character preceding extension 2' |
109 | 109 | ); |
110 | 110 | } |
— | — | @@ -111,7 +111,7 @@ |
112 | 112 | function testDotAtEnd() { |
113 | 113 | $this->assertEquals( |
114 | 114 | '', |
115 | | - WebRequest::findIE6Extension( '.' ), |
| 115 | + IEUrlExtension::findIE6Extension( '.' ), |
116 | 116 | 'Dot at end of string' |
117 | 117 | ); |
118 | 118 | } |
Index: trunk/phase3/includes/WebRequest.php |
— | — | @@ -772,25 +772,23 @@ |
773 | 773 | * message or redirect to a safer URL. Returns true if the URL is OK, and |
774 | 774 | * false if an error message has been shown and the request should be aborted. |
775 | 775 | */ |
776 | | - public function checkUrlExtension() { |
777 | | - $query = isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : ''; |
778 | | - if ( self::isUrlExtensionBad( $query ) ) { |
| 776 | + public function checkUrlExtension( $extWhitelist = array() ) { |
| 777 | + global $wgScriptExtension; |
| 778 | + $extWhitelist[] = ltrim( $wgScriptExtension, '.' ); |
| 779 | + if ( IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist ) ) { |
779 | 780 | if ( !$this->wasPosted() ) { |
780 | | - if ( $this->attemptExtensionSecurityRedirect() ) { |
| 781 | + $newUrl = IEUrlExtension::fixUrlForIE6( |
| 782 | + $this->getFullRequestURL(), $extWhitelist ); |
| 783 | + if ( $newUrl !== false ) { |
| 784 | + $this->doSecurityRedirect( $newUrl ); |
781 | 785 | return false; |
782 | 786 | } |
783 | 787 | } |
784 | 788 | wfHttpError( 403, 'Forbidden', |
785 | | - 'Invalid file extension found in the query string.' ); |
| 789 | + 'Invalid file extension found in the path info or query string.' ); |
786 | 790 | |
787 | 791 | return false; |
788 | 792 | } |
789 | | - |
790 | | - if ( $this->isPathInfoBad() ) { |
791 | | - wfHttpError( 403, 'Forbidden', |
792 | | - 'Invalid file extension found in PATH_INFO.' ); |
793 | | - return false; |
794 | | - } |
795 | 793 | return true; |
796 | 794 | } |
797 | 795 | |
— | — | @@ -798,12 +796,7 @@ |
799 | 797 | * Attempt to redirect to a URL with a QUERY_STRING that's not dangerous in |
800 | 798 | * IE 6. Returns true if it was successful, false otherwise. |
801 | 799 | */ |
802 | | - protected function attemptExtensionSecurityRedirect() { |
803 | | - $url = self::fixUrlForIE6( $this->getFullRequestURL() ); |
804 | | - if ( $url === false ) { |
805 | | - return false; |
806 | | - } |
807 | | - |
| 800 | + protected function doSecurityRedirect( $url ) { |
808 | 801 | header( 'Location: ' . $url ); |
809 | 802 | header( 'Content-Type: text/html' ); |
810 | 803 | $encUrl = htmlspecialchars( $url ); |
— | — | @@ -844,162 +837,16 @@ |
845 | 838 | * Also checks for anything that looks like a file extension at the end of |
846 | 839 | * QUERY_STRING, since IE 6 and earlier will use this to get the file type |
847 | 840 | * if there was no dot before the question mark (bug 28235). |
| 841 | + * |
| 842 | + * @deprecated Use checkUrlExtension(). |
848 | 843 | */ |
849 | | - public function isPathInfoBad() { |
| 844 | + public function isPathInfoBad( $extWhitelist = array() ) { |
850 | 845 | global $wgScriptExtension; |
851 | | - |
852 | | - if ( $this->isQueryStringBad() ) { |
853 | | - return true; |
854 | | - } |
855 | | - |
856 | | - if ( !isset( $_SERVER['PATH_INFO'] ) ) { |
857 | | - return false; |
858 | | - } |
859 | | - $pi = $_SERVER['PATH_INFO']; |
860 | | - $dotPos = strrpos( $pi, '.' ); |
861 | | - if ( $dotPos === false ) { |
862 | | - return false; |
863 | | - } |
864 | | - $ext = substr( $pi, $dotPos ); |
865 | | - return !in_array( $ext, array( $wgScriptExtension, '.php', '.php5' ) ); |
| 846 | + $extWhitelist[] = ltrim( $wgScriptExtension, '.' ); |
| 847 | + return IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist ); |
866 | 848 | } |
867 | | - |
868 | | - /** |
869 | | - * Determine what extension IE6 will infer from a certain query string. |
870 | | - * If the URL has an extension before the question mark, IE6 will use |
871 | | - * that and ignore the query string, but per the comment at |
872 | | - * isPathInfoBad() we don't have a reliable way to determine the URL, |
873 | | - * so isPathInfoBad() just passes in the query string for $url. |
874 | | - * All entry points have safe extensions (php, php5) anyway, so |
875 | | - * checking the query string is possibly overly paranoid but never |
876 | | - * insecure. |
877 | | - * |
878 | | - * The criteria for finding an extension are as follows: |
879 | | - * - a possible extension is a dot followed by one or more characters not |
880 | | - * in <>\"/:|?.# |
881 | | - * - if we find a possible extension followed by the end of the string or |
882 | | - * a #, that's our extension |
883 | | - * - if we find a possible extension followed by a ?, that's our extension |
884 | | - * - UNLESS it's exe, dll or cgi, in which case we ignore it and continue |
885 | | - * searching for another possible extension |
886 | | - * - if we find a possible extension followed by a dot or another illegal |
887 | | - * character, we ignore it and continue searching |
888 | | - * |
889 | | - * @param $url string URL |
890 | | - * @return mixed Detected extension (string), or false if none found |
891 | | - */ |
892 | | - public static function findIE6Extension( $url ) { |
893 | | - $pos = 0; |
894 | | - $hashPos = strpos( $url, '#' ); |
895 | | - if ( $hashPos !== false ) { |
896 | | - $urlLength = $hashPos; |
897 | | - } else { |
898 | | - $urlLength = strlen( $url ); |
899 | | - } |
900 | | - $remainingLength = $urlLength; |
901 | | - while ( $remainingLength > 0 ) { |
902 | | - // Skip ahead to the next dot |
903 | | - $pos += strcspn( $url, '.', $pos, $remainingLength ); |
904 | | - if ( $pos >= $urlLength ) { |
905 | | - // End of string, we're done |
906 | | - return false; |
907 | | - } |
908 | | - |
909 | | - // We found a dot. Skip past it |
910 | | - $pos++; |
911 | | - $remainingLength = $urlLength - $pos; |
912 | 849 | |
913 | | - // Check for illegal characters in our prospective extension, |
914 | | - // or for another dot |
915 | | - $nextPos = $pos + strcspn( $url, "<>\\\"/:|?*.", $pos, $remainingLength ); |
916 | | - if ( $nextPos >= $urlLength ) { |
917 | | - // No illegal character or next dot |
918 | | - // We have our extension |
919 | | - return substr( $url, $pos, $urlLength - $pos ); |
920 | | - } |
921 | | - if ( $url[$nextPos] === '?' ) { |
922 | | - // We've found a legal extension followed by a question mark |
923 | | - // If the extension is NOT exe, dll or cgi, return it |
924 | | - $extension = substr( $url, $pos, $nextPos - $pos ); |
925 | | - if ( strcasecmp( $extension, 'exe' ) && strcasecmp( $extension, 'dll' ) && |
926 | | - strcasecmp( $extension, 'cgi' ) ) |
927 | | - { |
928 | | - return $extension; |
929 | | - } |
930 | | - // Else continue looking |
931 | | - } |
932 | | - // We found an illegal character or another dot |
933 | | - // Skip to that character and continue the loop |
934 | | - $pos = $nextPos + 1; |
935 | | - $remainingLength = $urlLength - $pos; |
936 | | - } |
937 | | - return false; |
938 | | - } |
939 | | - |
940 | 850 | /** |
941 | | - * Check for a bad query string, which IE 6 will use as a potentially |
942 | | - * insecure cache file extension. See bug 28235. Returns true if the |
943 | | - * request should be disallowed. |
944 | | - * |
945 | | - * @return Boolean |
946 | | - */ |
947 | | - public function isQueryStringBad() { |
948 | | - if ( !isset( $_SERVER['QUERY_STRING'] ) ) { |
949 | | - return false; |
950 | | - } |
951 | | - return self::isUrlExtensionBad( $_SERVER['QUERY_STRING'] ); |
952 | | - } |
953 | | - |
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 ); |
963 | | - if ( strval( $extension ) === '' ) { |
964 | | - /* No extension or empty extension (false/'') */ |
965 | | - return false; |
966 | | - } |
967 | | - |
968 | | - /* Only consider the extension understood by IE to be potentially |
969 | | - * dangerous if it is made of normal characters (so it is more |
970 | | - * likely to be registered with an application) |
971 | | - * Compromise with api.php convenience. Considers for instance |
972 | | - * that no sane application will register a dangerous file type |
973 | | - * in an extension containing an ampersand. |
974 | | - */ |
975 | | - return (bool)preg_match( '/^[a-zA-Z0-9_-]+$/', $extension ); |
976 | | - } |
977 | | - |
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 | | - /** |
1004 | 851 | * Parse the Accept-Language header sent by the client into an array |
1005 | 852 | * @return array( languageCode => q-value ) sorted by q-value in descending order |
1006 | 853 | * May contain the "language" '*', which applies to languages other than those explicitly listed. |
— | — | @@ -1235,7 +1082,11 @@ |
1236 | 1083 | return $this->session; |
1237 | 1084 | } |
1238 | 1085 | |
1239 | | - public function isPathInfoBad() { |
| 1086 | + public function isPathInfoBad( $extWhitelist = array() ) { |
1240 | 1087 | return false; |
1241 | 1088 | } |
| 1089 | + |
| 1090 | + public function checkUrlExtension( $extWhitelist = array() ) { |
| 1091 | + return true; |
| 1092 | + } |
1242 | 1093 | } |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -506,6 +506,7 @@ |
507 | 507 | 'CSSJanus' => 'includes/libs/CSSJanus.php', |
508 | 508 | 'CSSMin' => 'includes/libs/CSSMin.php', |
509 | 509 | 'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php', |
| 510 | + 'IEUrlExtension' => 'includes/libs/IEUrlExtension.php', |
510 | 511 | 'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php', |
511 | 512 | |
512 | 513 | # includes/media |
Index: trunk/phase3/includes/libs/IEUrlExtension.php |
— | — | @@ -0,0 +1,247 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Internet Explorer derives a cache filename from a URL, and then in certain |
| 6 | + * circumstances, uses the extension of the resulting file to determine the |
| 7 | + * content type of the data, ignoring the Content-Type header. |
| 8 | + * |
| 9 | + * This can be a problem, especially when non-HTML content is sent by MediaWiki, |
| 10 | + * and Internet Explorer interprets it as HTML, exposing an XSS vulnerability. |
| 11 | + * |
| 12 | + * Usually the script filename (e.g. api.php) is present in the URL, and this |
| 13 | + * makes Internet Explorer think the extension is a harmless script extension. |
| 14 | + * But Internet Explorer 6 and earlier allows the script extension to be |
| 15 | + * obscured by encoding the dot as "%2E". |
| 16 | + * |
| 17 | + * This class contains functions which help in detecting and dealing with this |
| 18 | + * situation. |
| 19 | + * |
| 20 | + * Checking the URL for a bad extension is somewhat complicated due to the fact |
| 21 | + * that CGI doesn't provide a standard method to determine the URL. Instead it |
| 22 | + * is necessary to pass a subset of $_SERVER variables, which we then attempt |
| 23 | + * to use to guess parts of the URL. |
| 24 | + */ |
| 25 | +class IEUrlExtension { |
| 26 | + /** |
| 27 | + * Check a subset of $_SERVER (or the whole of $_SERVER if you like) |
| 28 | + * to see if it indicates that the request was sent with a bad file |
| 29 | + * extension. Returns true if the request should be denied or modified, |
| 30 | + * false otherwise. The relevant $_SERVER elements are: |
| 31 | + * |
| 32 | + * - SERVER_SOFTWARE |
| 33 | + * - REQUEST_URI |
| 34 | + * - QUERY_STRING |
| 35 | + * - PATH_INFO |
| 36 | + * |
| 37 | + * If the a variable is unset in $_SERVER, it should be unset in $vars. |
| 38 | + * |
| 39 | + * @param $vars A subset of $_SERVER. |
| 40 | + * @param $extWhitelist Extensions which are allowed, assumed harmless. |
| 41 | + */ |
| 42 | + public static function areServerVarsBad( $vars, $extWhitelist = array() ) { |
| 43 | + // Check QUERY_STRING or REQUEST_URI |
| 44 | + if ( isset( $vars['SERVER_SOFTWARE'] ) |
| 45 | + && isset( $vars['REQUEST_URI'] ) |
| 46 | + && self::haveUndecodedRequestUri( $vars['SERVER_SOFTWARE'] ) ) |
| 47 | + { |
| 48 | + $urlPart = $vars['REQUEST_URI']; |
| 49 | + } elseif ( isset( $vars['QUERY_STRING'] ) ) { |
| 50 | + $urlPart = $vars['QUERY_STRING']; |
| 51 | + } else { |
| 52 | + $urlPart = ''; |
| 53 | + } |
| 54 | + |
| 55 | + if ( self::isUrlExtensionBad( $urlPart, $extWhitelist ) ) { |
| 56 | + return true; |
| 57 | + } |
| 58 | + |
| 59 | + // Some servers have PATH_INFO but not REQUEST_URI, so we check both |
| 60 | + // to be on the safe side. |
| 61 | + if ( isset( $vars['PATH_INFO'] ) |
| 62 | + && self::isUrlExtensionBad( $vars['PATH_INFO'], $extWhitelist ) ) |
| 63 | + { |
| 64 | + return true; |
| 65 | + } |
| 66 | + |
| 67 | + // All checks passed |
| 68 | + return false; |
| 69 | + } |
| 70 | + |
| 71 | + /** |
| 72 | + * Given a right-hand portion of a URL, determine whether IE would detect |
| 73 | + * a potentially harmful file extension. |
| 74 | + * |
| 75 | + * @param $urlPart The right-hand portion of a URL |
| 76 | + * @param $extWhitelist An array of file extensions which may occur in this |
| 77 | + * URL, and which should be allowed. |
| 78 | + * @return bool |
| 79 | + */ |
| 80 | + public static function isUrlExtensionBad( $urlPart, $extWhitelist = array() ) { |
| 81 | + if ( strval( $urlPart ) === '' ) { |
| 82 | + return false; |
| 83 | + } |
| 84 | + |
| 85 | + $extension = self::findIE6Extension( $urlPart ); |
| 86 | + if ( strval( $extension ) === '' ) { |
| 87 | + // No extension or empty extension |
| 88 | + return false; |
| 89 | + } |
| 90 | + |
| 91 | + if ( in_array( $extension, array( 'php', 'php5' ) ) ) { |
| 92 | + // Script extension, OK |
| 93 | + return false; |
| 94 | + } |
| 95 | + if ( in_array( $extension, $extWhitelist ) ) { |
| 96 | + // Whitelisted extension |
| 97 | + return false; |
| 98 | + } |
| 99 | + |
| 100 | + if ( !preg_match( '/^[a-zA-Z0-9_-]+$/', $extension ) ) { |
| 101 | + // Non-alphanumeric extension, unlikely to be registered. |
| 102 | + // |
| 103 | + // The regex above is known to match all registered file extensions |
| 104 | + // in a default Windows XP installation. It's important to allow |
| 105 | + // extensions with ampersands and percent signs, since that reduces |
| 106 | + // the number of false positives substantially. |
| 107 | + return false; |
| 108 | + } |
| 109 | + |
| 110 | + // Possibly bad extension |
| 111 | + return true; |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * Returns a variant of $url which will pass isUrlExtensionBad() but has the |
| 116 | + * same GET parameters, or false if it can't figure one out. |
| 117 | + */ |
| 118 | + public static function fixUrlForIE6( $url, $extWhitelist = array() ) { |
| 119 | + $questionPos = strpos( $url, '?' ); |
| 120 | + if ( $questionPos === false ) { |
| 121 | + $beforeQuery = $url . '?'; |
| 122 | + $query = ''; |
| 123 | + } elseif ( $questionPos === strlen( $url ) - 1 ) { |
| 124 | + $beforeQuery = $url; |
| 125 | + $query = ''; |
| 126 | + } else { |
| 127 | + $beforeQuery = substr( $url, 0, $questionPos + 1 ); |
| 128 | + $query = substr( $url, $questionPos + 1 ); |
| 129 | + } |
| 130 | + |
| 131 | + // Multiple question marks cause problems. Encode the second and |
| 132 | + // subsequent question mark. |
| 133 | + $query = str_replace( '?', '%3E', $query ); |
| 134 | + // Append an invalid path character so that IE6 won't see the end of the |
| 135 | + // query string as an extension |
| 136 | + $query .= '&*'; |
| 137 | + // Put the URL back together |
| 138 | + $url = $beforeQuery . $query; |
| 139 | + if ( self::isUrlExtensionBad( $url, $extWhitelist ) ) { |
| 140 | + // Avoid a redirect loop |
| 141 | + return false; |
| 142 | + } |
| 143 | + return $url; |
| 144 | + } |
| 145 | + |
| 146 | + /** |
| 147 | + * Determine what extension IE6 will infer from a certain query string. |
| 148 | + * If the URL has an extension before the question mark, IE6 will use |
| 149 | + * that and ignore the query string, but per the comment at |
| 150 | + * isPathInfoBad() we don't have a reliable way to determine the URL, |
| 151 | + * so isPathInfoBad() just passes in the query string for $url. |
| 152 | + * All entry points have safe extensions (php, php5) anyway, so |
| 153 | + * checking the query string is possibly overly paranoid but never |
| 154 | + * insecure. |
| 155 | + * |
| 156 | + * The criteria for finding an extension are as follows: |
| 157 | + * - a possible extension is a dot followed by one or more characters not |
| 158 | + * in <>\"/:|?.# |
| 159 | + * - if we find a possible extension followed by the end of the string or |
| 160 | + * a #, that's our extension |
| 161 | + * - if we find a possible extension followed by a ?, that's our extension |
| 162 | + * - UNLESS it's exe, dll or cgi, in which case we ignore it and continue |
| 163 | + * searching for another possible extension |
| 164 | + * - if we find a possible extension followed by a dot or another illegal |
| 165 | + * character, we ignore it and continue searching |
| 166 | + * |
| 167 | + * @param $url string URL |
| 168 | + * @return mixed Detected extension (string), or false if none found |
| 169 | + */ |
| 170 | + public static function findIE6Extension( $url ) { |
| 171 | + $pos = 0; |
| 172 | + $hashPos = strpos( $url, '#' ); |
| 173 | + if ( $hashPos !== false ) { |
| 174 | + $urlLength = $hashPos; |
| 175 | + } else { |
| 176 | + $urlLength = strlen( $url ); |
| 177 | + } |
| 178 | + $remainingLength = $urlLength; |
| 179 | + while ( $remainingLength > 0 ) { |
| 180 | + // Skip ahead to the next dot |
| 181 | + $pos += strcspn( $url, '.', $pos, $remainingLength ); |
| 182 | + if ( $pos >= $urlLength ) { |
| 183 | + // End of string, we're done |
| 184 | + return false; |
| 185 | + } |
| 186 | + |
| 187 | + // We found a dot. Skip past it |
| 188 | + $pos++; |
| 189 | + $remainingLength = $urlLength - $pos; |
| 190 | + |
| 191 | + // Check for illegal characters in our prospective extension, |
| 192 | + // or for another dot |
| 193 | + $nextPos = $pos + strcspn( $url, "<>\\\"/:|?*.", $pos, $remainingLength ); |
| 194 | + if ( $nextPos >= $urlLength ) { |
| 195 | + // No illegal character or next dot |
| 196 | + // We have our extension |
| 197 | + return substr( $url, $pos, $urlLength - $pos ); |
| 198 | + } |
| 199 | + if ( $url[$nextPos] === '?' ) { |
| 200 | + // We've found a legal extension followed by a question mark |
| 201 | + // If the extension is NOT exe, dll or cgi, return it |
| 202 | + $extension = substr( $url, $pos, $nextPos - $pos ); |
| 203 | + if ( strcasecmp( $extension, 'exe' ) && strcasecmp( $extension, 'dll' ) && |
| 204 | + strcasecmp( $extension, 'cgi' ) ) |
| 205 | + { |
| 206 | + return $extension; |
| 207 | + } |
| 208 | + // Else continue looking |
| 209 | + } |
| 210 | + // We found an illegal character or another dot |
| 211 | + // Skip to that character and continue the loop |
| 212 | + $pos = $nextPos + 1; |
| 213 | + $remainingLength = $urlLength - $pos; |
| 214 | + } |
| 215 | + return false; |
| 216 | + } |
| 217 | + |
| 218 | + /** |
| 219 | + * When passed the value of $_SERVER['SERVER_SOFTWARE'], this function |
| 220 | + * returns true if that server is known to have a REQUEST_URI variable |
| 221 | + * with %2E not decoded to ".". On such a server, it is possible to detect |
| 222 | + * whether the script filename has been obscured. |
| 223 | + * |
| 224 | + * The function returns false if the server is not known to have this |
| 225 | + * behaviour. Microsoft IIS in particular is known to decode escaped script |
| 226 | + * filenames. |
| 227 | + * |
| 228 | + * SERVER_SOFTWARE typically contains either a plain string such as "Zeus", |
| 229 | + * or a specification in the style of a User-Agent header, such as |
| 230 | + * "Apache/1.3.34 (Unix) mod_ssl/2.8.25 OpenSSL/0.9.8a PHP/4.4.2" |
| 231 | + * |
| 232 | + * @param $serverSoftware |
| 233 | + * @return bool |
| 234 | + * |
| 235 | + */ |
| 236 | + public static function haveUndecodedRequestUri( $serverSoftware ) { |
| 237 | + static $whitelist = array( |
| 238 | + 'Apache', |
| 239 | + 'Zeus', |
| 240 | + 'LiteSpeed' ); |
| 241 | + if ( preg_match( '/^(.*?)($|\/| )/', $serverSoftware, $m ) ) { |
| 242 | + return in_array( $m[1], $whitelist ); |
| 243 | + } else { |
| 244 | + return false; |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | +} |
Property changes on: trunk/phase3/includes/libs/IEUrlExtension.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 249 | + native |
Index: trunk/phase3/img_auth.php |
— | — | @@ -42,14 +42,20 @@ |
43 | 43 | wfForbidden('img-auth-accessdenied','img-auth-public'); |
44 | 44 | } |
45 | 45 | |
| 46 | +$matches = WebRequest::getPathInfo(); |
| 47 | +$path = $matches['title']; |
| 48 | + |
46 | 49 | // Check for bug 28235: QUERY_STRING overriding the correct extension |
47 | | -if ( $wgRequest->isQueryStringBad() ) |
| 50 | +$dotPos = strpos( $path, '.' ); |
| 51 | +$whitelist = array(); |
| 52 | +if ( $dotPos !== false ) { |
| 53 | + $whitelist[] = substr( $path, $dotPos + 1 ); |
| 54 | +} |
| 55 | +if ( !$wgRequest->checkUrlExtension( $whitelist ) ) |
48 | 56 | { |
49 | | - wfForbidden( 'img-auth-accessdenied', 'img-auth-bad-query-string' ); |
50 | | -} |
| 57 | + return; |
| 58 | +} |
51 | 59 | |
52 | | -$matches = WebRequest::getPathInfo(); |
53 | | -$path = $matches['title']; |
54 | 60 | $filename = realpath( $wgUploadDirectory . $path ); |
55 | 61 | $realUpload = realpath( $wgUploadDirectory ); |
56 | 62 | |