Page MenuHomePhabricator

omnibus-v2-REL1_23.patch

Authored By
demon
Dec 17 2015, 5:08 PM
Size
34 KB
Referenced Files
None
Subscribers
None

omnibus-v2-REL1_23.patch

From 2da2ef21191cabffc2c70b1cfca116fd7bdd0e02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gerg=C5=91=20Tisza?= <tgr.huwiki@gmail.com>
Date: Sat, 21 Nov 2015 11:51:02 -0800
Subject: [PATCH 01/10] Use hash_equals in User::matchEditToken
There is no point in using hash_equals for the return value if we
do a normal comparison before.
Bug: T119309
Change-Id: Ia44ec5ed492105b27d0fddd845d58d27a29dc072
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/User.php | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/includes/User.php b/includes/User.php
index a0d0f0c..95724cc 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -3808,10 +3808,11 @@ class User {
*/
public function matchEditToken( $val, $salt = '', $request = null ) {
$sessionToken = $this->getEditToken( $salt, $request );
- if ( $val != $sessionToken ) {
+ $equals = hash_equals( $sessionToken, $val );
+ if ( !$equals ) {
wfDebug( "User::matchEditToken: broken session data\n" );
}
- return $val == $sessionToken;
+ return $equals;
}
/**
@@ -3825,7 +3826,7 @@ class User {
*/
public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
$sessionToken = $this->getEditToken( $salt, $request );
- return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
+ return hash_equals( substr( $sessionToken, 0, 32 ), substr( $val, 0, 32 ) );
}
/**
--
2.6.4
From b4ac2617831d0a27c667394742d9f5b9f0932620 Mon Sep 17 00:00:00 2001
From: Roan Kattouw <roan.kattouw@gmail.com>
Date: Fri, 6 Nov 2015 12:55:16 -0800
Subject: [PATCH 02/10] SECURITY: Work around CURL insanity breaking POST
parameters that start with '@'
CURL has a "feature" where passing array( 'foo' => '@bar' )
in CURLOPT_POSTFIELDS results in the contents of the file named "bar"
being POSTed. This makes it impossible to POST the literal string "@bar",
because array( 'foo' => '%40bar' ) gets double-encoded to foo=%2540bar.
Disable this "feature" by setting CURLOPT_SAFE_UPLOAD to true,
if available. According to the PHP manual, this option became
available in 5.5 and started defaulting to true in 5.6.
However, we support versions as low as 5.3, and this option
doesn't exist at all in 5.6.99-hhvm, which we run in production.
For versions where this option is not available (pre-5.5 versions
and HHVM), serialize POSTFIELDS arrays to strings. This works
around the issue because the '@' "feature" only works
for arrays, not strings, as of PHP 5.2. (We don't support pre-5.2
versions, and I've verified 5.6.99-hhvm behaves this way as well.)
Bug: T118032
Change-Id: I3f996e2eb87c7bd3b94ca9d3cc14a3e12f34f241
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/HttpFunctions.php | 17 ++++++++++++++++-
includes/libs/MultiHttpClient.php | 13 +++++++++++++
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 9db2891..9ded913 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -758,7 +758,22 @@ class CurlHttpRequest extends MWHttpRequest {
$this->curlOptions[CURLOPT_HEADER] = true;
} elseif ( $this->method == 'POST' ) {
$this->curlOptions[CURLOPT_POST] = true;
- $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData;
+ $postData = $this->postData;
+ // Don't interpret POST parameters starting with '@' as file uploads, because this
+ // makes it impossible to POST plain values starting with '@' (and causes security
+ // issues potentially exposing the contents of local files).
+ // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
+ // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
+ if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
+ $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
+ } else if ( is_array( $postData ) ) {
+ // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
+ // is an array, but not if it's a string. So convert $req['body'] to a string
+ // for safety.
+ $postData = wfArrayToCgi( $postData );
+ }
+ $this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
+
// Suppress 'Expect: 100-continue' header, as some servers
// will reject it with a 417 and Curl won't auto retry
// with HTTP 1.0 fallback
diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php
index 340e7f4..2087e9d 100644
--- a/includes/libs/MultiHttpClient.php
+++ b/includes/libs/MultiHttpClient.php
@@ -310,6 +310,19 @@ class MultiHttpClient {
);
} elseif ( $req['method'] === 'POST' ) {
curl_setopt( $ch, CURLOPT_POST, 1 );
+ // Don't interpret POST parameters starting with '@' as file uploads, because this
+ // makes it impossible to POST plain values starting with '@' (and causes security
+ // issues potentially exposing the contents of local files).
+ // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
+ // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
+ if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
+ curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
+ } else if ( is_array( $req['body'] ) ) {
+ // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
+ // is an array, but not if it's a string. So convert $req['body'] to a string
+ // for safety.
+ $req['body'] = wfArrayToCgi( $req['body'] );
+ }
curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
} else {
if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
--
2.6.4
From 11cc1c2824f320cfd49c2f412bee39522a2d84e9 Mon Sep 17 00:00:00 2001
From: Brad Jorsch <bjorsch@wikimedia.org>
Date: Wed, 14 Oct 2015 17:53:09 -0400
Subject: [PATCH 03/10] [SECURITY] 0-pad to length in random string generation
Otherwise shorter strings might be generated.
Bug: T115522
Change-Id: I3569218ea840e9de7a3fe458acf474e3dac6d1ab
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/User.php | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/includes/User.php b/includes/User.php
index 95724cc..6f4ca15 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -958,11 +958,10 @@ class User {
// Decide the final password length based on our min password length, stopping at a minimum of 10 chars
$length = max( 10, $wgMinimalPasswordLength );
// Multiply by 1.25 to get the number of hex characters we need
- $length = $length * 1.25;
// Generate random hex chars
- $hex = MWCryptRand::generateHex( $length );
+ $hex = MWCryptRand::generateHex( ceil( $length * 1.25 ) );
// Convert from base 16 to base 32 to get a proper password like string
- return wfBaseConvert( $hex, 16, 32 );
+ return substr( wfBaseConvert( $hex, 16, 32, $length ), -$length );
}
/**
--
2.6.4
From 28710c0195a38d9adb1d1ed0582b5f83fd3891fa Mon Sep 17 00:00:00 2001
From: Chad Horohoe <chadh@wikimedia.org>
Date: Tue, 15 Dec 2015 11:37:21 -0800
Subject: [PATCH 04/10] Fix IP::toHex for IPv4 addresses with a double/triple 0
block
Bug: T97897
Change-Id: I5c0a37be42ae2c5091ead487a6d19f6e0dd89b36
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/utils/IP.php | 22 ++++++++++++++++------
tests/phpunit/includes/utils/IPTest.php | 33 ++++++++++++++++++++++++++++-----
2 files changed, 44 insertions(+), 11 deletions(-)
diff --git a/includes/utils/IP.php b/includes/utils/IP.php
index 871f71b..9390dd8 100644
--- a/includes/utils/IP.php
+++ b/includes/utils/IP.php
@@ -127,8 +127,9 @@ class IP {
/**
* Convert an IP into a verbose, uppercase, normalized form.
- * IPv6 addresses in octet notation are expanded to 8 words.
- * IPv4 addresses are just trimmed.
+ * Both IPv4 and IPv6 addresses are trimmed. Additionally,
+ * IPv6 addresses in octet notation are expanded to 8 words;
+ * IPv4 addresses have leading zeros, in each octet, removed.
*
* @param string $ip IP address in quad or octet form (CIDR or not).
* @return String
@@ -138,8 +139,16 @@ class IP {
if ( $ip === '' ) {
return null;
}
- if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) {
- return $ip; // nothing else to do for IPv4 addresses or invalid ones
+ /* If not an IP, just return trimmed value, since sanitizeIP() is called
+ * in a number of contexts where usernames are supplied as input.
+ */
+ if ( !self::isIPAddress($ip) ) {
+ return $ip;
+ }
+ if ( self::isIPv4( $ip ) ) {
+ // Remove leading 0's from octet representation of IPv4 address
+ $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
+ return $ip;
}
// Remove any whitespaces, convert to upper case
$ip = strtoupper( $ip );
@@ -489,8 +498,9 @@ class IP {
if ( self::isIPv6( $ip ) ) {
$n = self::toUnsigned6( $ip );
} else {
- // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08)
- $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip );
+ // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
+ // also double/triple 0 needs to be changed to just a single 0 for ip2long.
+ $ip = self::sanitizeIP( $ip );
$n = ip2long( $ip );
if ( $n < 0 ) {
$n += pow( 2, 32 );
diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php
index 1267268..e1cdf83 100644
--- a/tests/phpunit/includes/utils/IPTest.php
+++ b/tests/phpunit/includes/utils/IPTest.php
@@ -270,12 +270,34 @@ class IPTest extends MediaWikiTestCase {
}
/**
- * Improve IP::sanitizeIP() code coverage
- * @todo Most probably incomplete
+ * @covers IP::sanitizeIP
+ * @dataProvider provideSanitizeIP
*/
- public function testSanitizeIP() {
- $this->assertNull( IP::sanitizeIP( '' ) );
- $this->assertNull( IP::sanitizeIP( ' ' ) );
+ public function testSanitizeIP( $expected, $input ) {
+ $result = IP::sanitizeIP( $input );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Provider for IP::testSanitizeIP()
+ */
+ public static function provideSanitizeIP() {
+ return array(
+ array( '0.0.0.0', '0.0.0.0' ),
+ array( '0.0.0.0', '00.00.00.00' ),
+ array( '0.0.0.0', '000.000.000.000' ),
+ array( '141.0.11.253', '141.000.011.253' ),
+ array( '1.2.4.5', '1.2.4.5' ),
+ array( '1.2.4.5', '01.02.04.05' ),
+ array( '1.2.4.5', '001.002.004.005' ),
+ array( '10.0.0.1', '010.0.000.1' ),
+ array( '80.72.250.4', '080.072.250.04' ),
+ array( 'Foo.1000.00', 'Foo.1000.00'),
+ array( 'Bar.01', 'Bar.01'),
+ array( 'Bar.010', 'Bar.010'),
+ array( null, ''),
+ array( null, ' ')
+ );
}
/**
@@ -331,6 +353,7 @@ class IPTest extends MediaWikiTestCase {
array( '80000000', '128.0.0.0' ),
array( 'DEADCAFE', '222.173.202.254' ),
array( 'FFFFFFFF', '255.255.255.255' ),
+ array( '8D000BFD', '141.000.11.253' ),
array( false, 'IN.VA.LI.D' ),
array( 'v6-00000000000000000000000000000001', '::1' ),
array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ),
--
2.6.4
From 2131e01e36f3e22ec046bab777f9927e8d63e38e Mon Sep 17 00:00:00 2001
From: JuneHyeon Bae <devunt@gmail.com>
Date: Sat, 24 May 2014 19:48:02 +0900
Subject: [PATCH 05/10] Validates wgArticlePath does start with slash (/).
When relative URL used in $wgArticlePath, and $wgArticlePath does not
start with slash (/), raise FatalError.
Bug: T48998
Change-Id: Ic7cd6f774cff97081f4f35af351161170b4b26eb
---
includes/Setup.php | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/includes/Setup.php b/includes/Setup.php
index 1a82ac3..d0639f5 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -425,6 +425,20 @@ require_once "$IP/includes/normal/UtfNormalUtil.php";
require_once "$IP/includes/normal/UtfNormalDefines.php";
wfProfileOut( $fname . '-includes' );
+wfProfileIn( $fname . '-validation' );
+
+// T48998: Bail out early if $wgArticlePath is non-absolute
+if ( !preg_match( '/^https?:\/\/|\//', $wgArticlePath ) ) {
+ throw new FatalError(
+ 'If you use a relative URL on $wgArticlePath, it must start ' .
+ 'with a slash (/).<br><br>See ' .
+ '<a href="https://www.mediawiki.org/wiki/Manual:$wgArticlePath">' .
+ 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath</a>'
+ );
+}
+
+wfProfileOut( $fname . '-validation' );
+
wfProfileIn( $fname . '-defaults2' );
if ( $wgSecureLogin && substr( $wgServer, 0, 2 ) !== '//' ) {
$wgSecureLogin = false;
--
2.6.4
From eb3ff7ac587c96a0ba010997131383e548f9b35a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartosz=20Dziewo=C5=84ski?= <matma.rex@gmail.com>
Date: Wed, 11 Nov 2015 23:00:31 +0100
Subject: [PATCH 06/10] Really validate that $wgArticlePath starts with a slash
The regular expression wasn't entirely correct.
Follow-up to a4a3d0454069c25a24e2bfe732a665cc6a865878.
Bug: T48998
Change-Id: I08bdf2db20c1c3de55527fc812bcbb55fa23f7bc
---
includes/Setup.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/includes/Setup.php b/includes/Setup.php
index d0639f5..da67b66 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -428,12 +428,12 @@ wfProfileOut( $fname . '-includes' );
wfProfileIn( $fname . '-validation' );
// T48998: Bail out early if $wgArticlePath is non-absolute
-if ( !preg_match( '/^https?:\/\/|\//', $wgArticlePath ) ) {
+if ( !preg_match( '/^(https?:\/\/|\/)/', $wgArticlePath ) ) {
throw new FatalError(
- 'If you use a relative URL on $wgArticlePath, it must start ' .
- 'with a slash (/).<br><br>See ' .
+ 'If you use a relative URL for $wgArticlePath, it must start ' .
+ 'with a slash (<code>/</code>).<br><br>See ' .
'<a href="https://www.mediawiki.org/wiki/Manual:$wgArticlePath">' .
- 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath</a>'
+ 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath</a>.'
);
}
--
2.6.4
From ec236bc08c9a1e19a61419bf3e50b1bb76f919cc Mon Sep 17 00:00:00 2001
From: Timo Tijhof <krinklemail@gmail.com>
Date: Fri, 19 Jun 2015 20:56:36 +0100
Subject: [PATCH 07/10] MediaWiki.php: Factor out tryNormaliseRedirect
This is in preparation for fixing T67402, which requires adding
logic inside this condition block. However the to-be-added code
will influences whether or not a redirect should be made.
In case a redirect is not made, it has to fall through to the next
'elseif' handler in MediaWiki::performRequest(), which is not possible
from inside the 'if' block.
Hence, move it out in a separate block and use a boolean return value
to communicate whether the case has been handled.
This also allows us to unit test this thing. Which is desperately
needed. Albeit ugly as it requires lots of mocking.
Change-Id: If3157f2ff1fd3ab2ca20a5d1f550d864ea62c493
---
includes/Wiki.php | 147 +++++++++++++++++------------
tests/phpunit/includes/MediaWikiTest.php | 157 +++++++++++++++++++++++++++++++
2 files changed, 243 insertions(+), 61 deletions(-)
create mode 100644 tests/phpunit/includes/MediaWikiTest.php
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 95beb20..5de313b 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -235,76 +235,101 @@ class MediaWiki {
$output->redirect( $url, 301 );
} else {
$this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
- wfProfileOut( __METHOD__ );
throw new BadTitleError();
}
- // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
- } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
- && ( $request->getVal( 'title' ) === null
- || $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
- && !count( $request->getValueNames( array( 'action', 'title' ) ) )
- && wfRunHooks( 'TestCanonicalRedirect', array( $request, $title, $output ) )
- ) {
- if ( $title->isSpecialPage() ) {
- list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
- if ( $name ) {
- $title = SpecialPage::getTitleFor( $name, $subpage );
- }
- }
- $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
- // Redirect to canonical url, make it a 301 to allow caching
- if ( $targetUrl == $request->getFullRequestURL() ) {
- $message = "Redirect loop detected!\n\n" .
- "This means the wiki got confused about what page was " .
- "requested; this sometimes happens when moving a wiki " .
- "to a new server or changing the server configuration.\n\n";
-
- if ( $wgUsePathInfo ) {
- $message .= "The wiki is trying to interpret the page " .
- "title from the URL path portion (PATH_INFO), which " .
- "sometimes fails depending on the web server. Try " .
- "setting \"\$wgUsePathInfo = false;\" in your " .
- "LocalSettings.php, or check that \$wgArticlePath " .
- "is correct.";
+ // Handle any other redirects.
+ // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
+ } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
+
+ // Special pages
+ if ( NS_SPECIAL == $title->getNamespace() ) {
+ // Actions that need to be made when we have a special pages
+ SpecialPageFactory::executePath( $title, $this->context );
+ } else {
+ // ...otherwise treat it as an article view. The article
+ // may still be a wikipage redirect to another article or URL.
+ $article = $this->initializeArticle();
+ if ( is_object( $article ) ) {
+ $this->performAction( $article, $requestTitle );
+ } elseif ( is_string( $article ) ) {
+ $output->redirect( $article );
} else {
- $message .= "Your web server was detected as possibly not " .
- "supporting URL path components (PATH_INFO) correctly; " .
- "check your LocalSettings.php for a customized " .
- "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
- "to true.";
+ throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
+ . " returned neither an object nor a URL" );
}
- throw new HttpError( 500, $message );
- } else {
- $output->setSquidMaxage( 1200 );
- $output->redirect( $targetUrl, '301' );
- }
- // Special pages
- } elseif ( NS_SPECIAL == $title->getNamespace() ) {
- $pageView = true;
- // Actions that need to be made when we have a special pages
- SpecialPageFactory::executePath( $title, $this->context );
- } else {
- // ...otherwise treat it as an article view. The article
- // may be a redirect to another article or URL.
- $article = $this->initializeArticle();
- if ( is_object( $article ) ) {
- $pageView = true;
- $this->performAction( $article, $requestTitle );
- } elseif ( is_string( $article ) ) {
- $output->redirect( $article );
- } else {
- wfProfileOut( __METHOD__ );
- throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
- . " returned neither an object nor a URL" );
}
}
+ }
+
+ /**
+ * Handle redirects for uncanonical title requests.
+ *
+ * Handles:
+ * - Redirect loops.
+ * - No title in URL.
+ * - $wgUsePathInfo URLs.
+ * - URLs with a variant.
+ * - Other non-standard URLs (as long as they have no extra query parameters).
+ *
+ * Behaviour:
+ * - Normalise title values:
+ * /wiki/Foo%20Bar -> /wiki/Foo_Bar
+ * - Normalise empty title:
+ * /wiki/ -> /wiki/Main
+ * /w/index.php?title= -> /wiki/Main
+ * - Don't redirect anything with query parameters other than 'title' or 'action=view'.
+ *
+ * @return bool True if a redirect was set.
+ */
+ private function tryNormaliseRedirect( $title ) {
+ global $wgUsePathInfo;
- if ( $pageView ) {
- // Promote user to any groups they meet the criteria for
- $user->addAutopromoteOnceGroups( 'onView' );
+ $request = $this->context->getRequest();
+ $output = $this->context->getOutput();
+
+ if ( $request->getVal( 'action', 'view' ) != 'view'
+ || $request->wasPosted()
+ || ( $request->getVal( 'title' ) !== null
+ && $title->getPrefixedDBkey() == $request->getVal( 'title' ) )
+ || count( $request->getValueNames( array( 'action', 'title' ) ) )
+ || !Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) )
+ ) {
+ return false;
}
- wfProfileOut( __METHOD__ );
+ if ( $title->isSpecialPage() ) {
+ list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
+ if ( $name ) {
+ $title = SpecialPage::getTitleFor( $name, $subpage );
+ }
+ }
+ // Redirect to canonical url, make it a 301 to allow caching
+ $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
+ if ( $targetUrl == $request->getFullRequestURL() ) {
+ $message = "Redirect loop detected!\n\n" .
+ "This means the wiki got confused about what page was " .
+ "requested; this sometimes happens when moving a wiki " .
+ "to a new server or changing the server configuration.\n\n";
+
+ if ( $wgUsePathInfo ) {
+ $message .= "The wiki is trying to interpret the page " .
+ "title from the URL path portion (PATH_INFO), which " .
+ "sometimes fails depending on the web server. Try " .
+ "setting \"\$wgUsePathInfo = false;\" in your " .
+ "LocalSettings.php, or check that \$wgArticlePath " .
+ "is correct.";
+ } else {
+ $message .= "Your web server was detected as possibly not " .
+ "supporting URL path components (PATH_INFO) correctly; " .
+ "check your LocalSettings.php for a customized " .
+ "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
+ "to true.";
+ }
+ throw new HttpError( 500, $message );
+ }
+ $output->setSquidMaxage( 1200 );
+ $output->redirect( $targetUrl, '301' );
+ return true;
}
/**
diff --git a/tests/phpunit/includes/MediaWikiTest.php b/tests/phpunit/includes/MediaWikiTest.php
new file mode 100644
index 0000000..df94d3e
--- /dev/null
+++ b/tests/phpunit/includes/MediaWikiTest.php
@@ -0,0 +1,157 @@
+<?php
+
+class MediaWikiTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgServer' => 'http://example.org',
+ 'wgScriptPath' => '/w',
+ 'wgScript' => '/w/index.php',
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgActionPaths' => array(),
+ ) );
+ }
+
+ public static function provideTryNormaliseRedirect() {
+ return array(
+ array(
+ // View: Canonical
+ 'url' => 'http://example.org/wiki/Foo_Bar',
+ 'query' => array(),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Escaped title
+ 'url' => 'http://example.org/wiki/Foo%20Bar',
+ 'query' => array(),
+ 'title' => 'Foo_Bar',
+ 'redirect' => 'http://example.org/wiki/Foo_Bar',
+ ),
+ array(
+ // View: Script path
+ 'url' => 'http://example.org/w/index.php?title=Foo_Bar',
+ 'query' => array( 'title' => 'Foo_Bar' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Script path with implicit title from page id
+ 'url' => 'http://example.org/w/index.php?curid=123',
+ 'query' => array( 'curid' => '123' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Script path with implicit title from revision id
+ 'url' => 'http://example.org/w/index.php?oldid=123',
+ 'query' => array( 'oldid' => '123' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Script path without title
+ 'url' => 'http://example.org/w/index.php',
+ 'query' => array(),
+ 'title' => 'Main_Page',
+ 'redirect' => 'http://example.org/wiki/Main_Page',
+ ),
+ array(
+ // View: Script path with empty title
+ 'url' => 'http://example.org/w/index.php?title=',
+ 'query' => array( 'title' => '' ),
+ 'title' => 'Main_Page',
+ 'redirect' => 'http://example.org/wiki/Main_Page',
+ ),
+ array(
+ // View: Index with escaped title
+ 'url' => 'http://example.org/w/index.php?title=Foo%20Bar',
+ 'query' => array( 'title' => 'Foo Bar' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => 'http://example.org/wiki/Foo_Bar',
+ ),
+ array(
+ // View: Script path with escaped title
+ 'url' => 'http://example.org/w/?title=Foo_Bar',
+ 'query' => array( 'title' => 'Foo_Bar' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Root path with escaped title
+ 'url' => 'http://example.org/?title=Foo_Bar',
+ 'query' => array( 'title' => 'Foo_Bar' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Canonical with redundant query
+ 'url' => 'http://example.org/wiki/Foo_Bar?action=view',
+ 'query' => array( 'action' => 'view' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // Edit: Canonical view url with action query
+ 'url' => 'http://example.org/wiki/Foo_Bar?action=edit',
+ 'query' => array( 'action' => 'edit' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // View: Index with action query
+ 'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=view',
+ 'query' => array( 'title' => 'Foo_Bar', 'action' => 'view' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ array(
+ // Edit: Index with action query
+ 'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=edit',
+ 'query' => array( 'title' => 'Foo_Bar', 'action' => 'edit' ),
+ 'title' => 'Foo_Bar',
+ 'redirect' => false,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideTryNormaliseRedirect
+ * @covers MediaWiki::tryNormaliseRedirect
+ */
+ public function testTryNormaliseRedirect( $url, $query, $title, $expectedRedirect = false ) {
+ // Set SERVER because interpolateTitle() doesn't use getRequestURL(),
+ // whereas tryNormaliseRedirect does().
+ $_SERVER['REQUEST_URI'] = $url;
+
+ $req = new FauxRequest( $query );
+ $req->setRequestURL( $url );
+ // This adds a virtual 'title' query parameter. Normally called from Setup.php
+ $req->interpolateTitle();
+
+ $titleObj = Title::newFromText( $title );
+
+ // Set global context since some involved code paths don't yet have context
+ $context = RequestContext::getMain();
+ $context->setRequest( $req );
+ $context->setTitle( $titleObj );
+
+ $mw = new MediaWiki( $context );
+
+ $method = new ReflectionMethod( $mw, 'tryNormaliseRedirect' );
+ $method->setAccessible( true );
+ $ret = $method->invoke( $mw, $titleObj );
+
+ $this->assertEquals(
+ $expectedRedirect !== false,
+ $ret,
+ 'Return true only when redirecting'
+ );
+
+ $this->assertEquals(
+ $expectedRedirect ?: '',
+ $context->getOutput()->getRedirect()
+ );
+ }
+}
--
2.6.4
From 47948ffe53c4eb389fa05f16ae3a2a162c5aa571 Mon Sep 17 00:00:00 2001
From: Aaron Schulz <aschulz@wikimedia.org>
Date: Mon, 24 Aug 2015 13:00:23 -0700
Subject: [PATCH 08/10] Fixed some doc errors in tryNormaliseRedirect()
Change-Id: I8f9397d05de1c0bae33497d1f9e3146939599380
---
includes/Wiki.php | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 5de313b..5495afe 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -279,9 +279,11 @@ class MediaWiki {
* /w/index.php?title= -> /wiki/Main
* - Don't redirect anything with query parameters other than 'title' or 'action=view'.
*
+ * @param Title $title
* @return bool True if a redirect was set.
+ * @throws HttpError
*/
- private function tryNormaliseRedirect( $title ) {
+ private function tryNormaliseRedirect( Title $title ) {
global $wgUsePathInfo;
$request = $this->context->getRequest();
--
2.6.4
From 28e16c32f13b4b878afa3e3091d592a19a60f347 Mon Sep 17 00:00:00 2001
From: csteipp <csteipp@wikimedia.org>
Date: Mon, 5 Oct 2015 16:58:42 -0700
Subject: [PATCH 09/10] SECURITY: Make Special:MyPage and friends fake redirect
to prevent info leak
This prevents a malicious person from using external resources on their
website to cause the victim's web browser to load
Special:MyPage -> User:Username, and then looking it up in the page hit
statistics in order to correlate IPs from the malicious person's server
log, with usernames on wiki.
This feature can be disabled with $wgHideIdentifiableRedirects.
Bug: T109724
Change-Id: Ia0e742dc92c77af4832174dfa24c6dcaa6ee80e9
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/DefaultSettings.php | 6 ++++
includes/Wiki.php | 44 ++++++++++++++++++++----
includes/specialpage/RedirectSpecialPage.php | 12 +++++++
includes/specials/SpecialMyRedirectPages.php | 50 ++++++++++++++++++++++++++++
4 files changed, 106 insertions(+), 6 deletions(-)
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 1201c3a..b8f1164 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -4268,6 +4268,12 @@ $wgWhitelistReadRegexp = false;
$wgEmailConfirmToEdit = false;
/**
+ * Should MediaWiki attempt to protect user's privacy when doing redirects?
+ * Keep this true if access counts to articles are made public.
+ */
+$wgHideIdentifiableRedirects = true;
+
+/**
* Permission keys given to users in each group.
*
* This is an array where the keys are all groups and each value is an
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 5495afe..76ab57a 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -54,6 +54,11 @@ class MediaWiki {
}
/**
+ * @var String Cache what action this request is
+ */
+ private $action;
+
+ /**
* @param IContextSource|null $context
*/
public function __construct( IContextSource $context = null ) {
@@ -145,13 +150,11 @@ class MediaWiki {
* @return string: action
*/
public function getAction() {
- static $action = null;
-
- if ( $action === null ) {
- $action = Action::getActionName( $this->context );
+ if ( $this->action === null ) {
+ $this->action = Action::getActionName( $this->context );
}
- return $action;
+ return $this->action;
}
/**
@@ -240,8 +243,37 @@ class MediaWiki {
// Handle any other redirects.
// Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
} elseif ( !$this->tryNormaliseRedirect( $title ) ) {
+ // Prevent information leak via Special:MyPage et al (T109724)
+ if ( $title->isSpecialPage() ) {
+ $specialPage = SpecialPageFactory::getPage( $title->getDBKey() );
+ if ( $specialPage instanceof RedirectSpecialPage
+ && $this->config->get( 'HideIdentifiableRedirects' )
+ && $specialPage->personallyIdentifiableTarget()
+ ) {
+ list( , $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBKey() );
+ $target = $specialPage->getRedirect( $subpage );
+ // target can also be true. We let that case fall through to normal processing.
+ if ( $target instanceof Title ) {
+ $query = $specialPage->getRedirectQuery() ?: array();
+ $request = new DerivativeRequest( $this->context->getRequest(), $query );
+ $request->setRequestURL( $this->context->getRequest()->getRequestURL() );
+ $this->context->setRequest( $request );
+ // Do not varnish cache these. May vary even for anons
+ $this->context->getOutput()->lowerCdnMaxage( 0 );
+ $this->context->setTitle( $target );
+ $wgTitle = $target;
+ // Reset action type cache. (Special pages have only view)
+ $this->action = null;
+ $title = $target;
+ $output->addJsConfigVars( array(
+ 'wgInternalRedirectTargetUrl' => $target->getFullURL(),
+ ) );
+ $output->addModules( 'mediawiki.action.view.redirect' );
+ }
+ }
+ }
- // Special pages
+ // Special pages ($title may have changed since if statement above)
if ( NS_SPECIAL == $title->getNamespace() ) {
// Actions that need to be made when we have a special pages
SpecialPageFactory::executePath( $title, $this->context );
diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php
index 3fbb0e5..939fed9 100644
--- a/includes/specialpage/RedirectSpecialPage.php
+++ b/includes/specialpage/RedirectSpecialPage.php
@@ -86,6 +86,18 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
? $params
: false;
}
+
+ /**
+ * Indicate if the target of this redirect can be used to identify
+ * a particular user of this wiki (e.g., if the redirect is to the
+ * user page of a User). See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return false;
+ }
}
/**
diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php
index 9b8d52b..65ac864 100644
--- a/includes/specials/SpecialMyRedirectPages.php
+++ b/includes/specials/SpecialMyRedirectPages.php
@@ -41,6 +41,16 @@ class SpecialMypage extends RedirectSpecialArticle {
return Title::makeTitle( NS_USER, $this->getUser()->getName() );
}
}
+
+ /**
+ * Target identifies a specific User. See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return true;
+ }
}
/**
@@ -60,6 +70,16 @@ class SpecialMytalk extends RedirectSpecialArticle {
return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() );
}
}
+
+ /**
+ * Target identifies a specific User. See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return true;
+ }
}
/**
@@ -77,6 +97,16 @@ class SpecialMycontributions extends RedirectSpecialPage {
function getRedirect( $subpage ) {
return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() );
}
+
+ /**
+ * Target identifies a specific User. See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return true;
+ }
}
/**
@@ -93,6 +123,16 @@ class SpecialMyuploads extends RedirectSpecialPage {
function getRedirect( $subpage ) {
return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() );
}
+
+ /**
+ * Target identifies a specific User. See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return true;
+ }
}
/**
@@ -111,4 +151,14 @@ class SpecialAllMyUploads extends RedirectSpecialPage {
return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() );
}
+
+ /**
+ * Target identifies a specific User. See T109724.
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function personallyIdentifiableTarget() {
+ return true;
+ }
}
--
2.6.4
From 9752a9920f27889e1f797801159fcc33f55b69ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartosz=20Dziewo=C5=84ski?= <matma.rex@gmail.com>
Date: Thu, 19 Nov 2015 17:13:13 -0500
Subject: [PATCH 10/10] Add $query to JavaScript redirect info
Bug: T109724
Change-Id: I57a8f75067365d3da6388d2f8f7fe95ed5e6f310
Signed-off-by: Chad Horohoe <chadh@wikimedia.org>
---
includes/Wiki.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 76ab57a..1aceb5b 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -266,7 +266,7 @@ class MediaWiki {
$this->action = null;
$title = $target;
$output->addJsConfigVars( array(
- 'wgInternalRedirectTargetUrl' => $target->getFullURL(),
+ 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ),
) );
$output->addModules( 'mediawiki.action.view.redirect' );
}
--
2.6.4

Event Timeline