diff options
Diffstat (limited to 'includes')
-rw-r--r-- | includes/DefaultSettings.php | 8 | ||||
-rw-r--r-- | includes/Hooks.php | 42 | ||||
-rw-r--r-- | includes/HttpFunctions.php | 17 | ||||
-rw-r--r-- | includes/MediaWiki.php | 44 | ||||
-rw-r--r-- | includes/Setup.php | 15 | ||||
-rw-r--r-- | includes/User.php | 7 | ||||
-rw-r--r-- | includes/debug/logger/LoggerFactory.php | 2 | ||||
-rw-r--r-- | includes/libs/MultiHttpClient.php | 13 | ||||
-rw-r--r-- | includes/libs/objectcache/APCBagOStuff.php | 54 | ||||
-rw-r--r-- | includes/specialpage/RedirectSpecialPage.php | 12 | ||||
-rw-r--r-- | includes/specials/SpecialExpandTemplates.php | 2 | ||||
-rw-r--r-- | includes/specials/SpecialMyLanguage.php | 11 | ||||
-rw-r--r-- | includes/specials/SpecialMyRedirectPages.php | 50 | ||||
-rw-r--r-- | includes/specials/SpecialSearch.php | 2 | ||||
-rw-r--r-- | includes/utils/IP.php | 22 |
15 files changed, 237 insertions, 64 deletions
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 268a8d19..919d05b8 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,7 +75,7 @@ $wgConfigRegistry = array( * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.26.0'; +$wgVersion = '1.26.1'; /** * Name of the site. It must be changed in LocalSettings.php @@ -4680,6 +4680,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/Hooks.php b/includes/Hooks.php index a4145624..90185816 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -193,34 +193,17 @@ class Hooks { $badhookmsg = null; $hook_args = array_merge( $hook, $args ); - set_error_handler( 'Hooks::hookErrorHandler' ); - // mark hook as deprecated, if deprecation version is specified if ( $deprecatedVersion !== null ) { wfDeprecated( "$event hook (used in $func)", $deprecatedVersion ); } - try { - $retval = call_user_func_array( $callback, $hook_args ); - } catch ( MWHookException $e ) { - $badhookmsg = $e->getMessage(); - } catch ( Exception $e ) { - restore_error_handler(); - throw $e; - } - - restore_error_handler(); + $retval = call_user_func_array( $callback, $hook_args ); // Process the return value. if ( is_string( $retval ) ) { // String returned means error. throw new FatalError( $retval ); - } elseif ( $badhookmsg !== null ) { - // Exception was thrown from Hooks::hookErrorHandler. - throw new MWException( - 'Detected bug in an extension! ' . - "Hook $func has invalid call signature; " . $badhookmsg - ); } elseif ( $retval === false ) { // False was returned. Stop processing, but no error. return false; @@ -229,27 +212,4 @@ class Hooks { return true; } - - /** - * Handle PHP errors issued inside a hook. Catch errors that have to do - * with a function expecting a reference, and pass all others through to - * MWExceptionHandler::handleError() for default processing. - * - * @since 1.18 - * - * @param int $errno Error number (unused) - * @param string $errstr Error message - * @throws MWHookException If the error has to do with the function signature - * @return bool - */ - public static function hookErrorHandler( $errno, $errstr ) { - if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { - throw new MWHookException( $errstr, $errno ); - } - - // Delegate unhandled errors to the default MW handler - return call_user_func_array( - 'MWExceptionHandler::handleError', func_get_args() - ); - } } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index bc5a9570..3dff9711 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -779,7 +779,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/MediaWiki.php b/includes/MediaWiki.php index fbacb250..2da2f6ce 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -37,6 +37,11 @@ class MediaWiki { private $config; /** + * @var String Cache what action this request is + */ + private $action; + + /** * @param IContextSource|null $context */ public function __construct( IContextSource $context = null ) { @@ -141,13 +146,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; } /** @@ -242,8 +245,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( $query ), + ) ); + $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/Setup.php b/includes/Setup.php index 70e8cde4..905a1d10 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -510,6 +510,21 @@ MWExceptionHandler::installHandler(); require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; + +$ps_validation = Profiler::instance()->scopedProfileIn( $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 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>.' + ); +} + +Profiler::instance()->scopedProfileOut( $ps_validation ); + $ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); if ( $wgScriptExtension !== '.php' || defined( 'MW_ENTRY_PHP5' ) ) { diff --git a/includes/User.php b/includes/User.php index 22c90cdd..199dd1dc 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1029,11 +1029,10 @@ class User implements IDBAccessObject { // 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 ); } /** @@ -4177,7 +4176,7 @@ class User implements IDBAccessObject { $salt, $request ?: $this->getRequest(), $timestamp ); - if ( $val != $sessionToken ) { + if ( !hash_equals( $sessionToken, $val ) ) { wfDebug( "User::matchEditToken: broken session data\n" ); } diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php index 0b6965ff..1e44b708 100644 --- a/includes/debug/logger/LoggerFactory.php +++ b/includes/debug/logger/LoggerFactory.php @@ -94,7 +94,7 @@ class LoggerFactory { * @return \\Psr\\Log\\LoggerInterface */ public static function getInstance( $channel ) { - if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { $message = ( 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' . "library</a> to be present. This library is not embedded directly in MediaWiki's " . diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 6af3ed51..5555cbcb 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -335,6 +335,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'] !== '' ) { diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0dbbaba9..35e05e80 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -27,22 +27,72 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + + /** + * @var bool If true, trust the APC implementation to serialize and + * deserialize objects correctly. If false, (de-)serialize in PHP. + */ + protected $nativeSerialize; + /** * @var string String to append to each APC key. This may be changed * whenever the handling of values is changed, to prevent existing code * from encountering older values which it cannot handle. - **/ - const KEY_SUFFIX = ':1'; + */ + const KEY_SUFFIX = ':2'; + + /** + * Constructor + * + * Available parameters are: + * - nativeSerialize: If true, pass objects to apc_store(), and trust it + * to serialize them correctly. If false, serialize + * all values in PHP. + * + * @param array $params + */ + public function __construct( array $params = array() ) { + parent::__construct( $params ); + + if ( isset( $params['nativeSerialize'] ) ) { + $this->nativeSerialize = $params['nativeSerialize']; + } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) { + // APCu has a memory corruption bug when the serializer is set to 'default'. + // See T120267, and upstream bug reports: + // - https://github.com/krakjoe/apcu/issues/38 + // - https://github.com/krakjoe/apcu/issues/35 + // - https://github.com/krakjoe/apcu/issues/111 + $this->logger->warning( + 'The APCu extension is loaded and the apc.serializer INI setting ' . + 'is set to "default". This can cause memory corruption! ' . + 'You should change apc.serializer to "php" instead. ' . + 'See <https://github.com/krakjoe/apcu/issues/38>.' + ); + $this->nativeSerialize = false; + } else { + $this->nativeSerialize = true; + } + } public function get( $key, &$casToken = null, $flags = 0 ) { $val = apc_fetch( $key . self::KEY_SUFFIX ); $casToken = $val; + if ( is_string( $val ) && !$this->nativeSerialize ) { + $val = $this->isInteger( $val ) + ? intval( $val ) + : unserialize( $val ); + } + return $val; } public function set( $key, $value, $exptime = 0 ) { + if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + apc_store( $key . self::KEY_SUFFIX, $value, $exptime ); return true; diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index 9129ee5d..5047354e 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -94,6 +94,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/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index b7582e6c..06eb2769 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage { } $config = $this->getConfig(); - if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + if ( $config->get( 'UseTidy' ) && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); } diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 3d8ff97b..d11fbe63 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -99,4 +99,15 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return $base; } } + + /** + * Target can identify a specific user's language preference. + * + * @see T109724 + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php index 5ef03f13..850b1f63 100644 --- a/includes/specials/SpecialMyRedirectPages.php +++ b/includes/specials/SpecialMyRedirectPages.php @@ -45,6 +45,16 @@ class SpecialMypage extends RedirectSpecialArticle { return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -68,6 +78,16 @@ class SpecialMytalk extends RedirectSpecialArticle { return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -90,6 +110,16 @@ class SpecialMycontributions extends RedirectSpecialPage { public 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; + } } /** @@ -110,6 +140,16 @@ class SpecialMyuploads extends RedirectSpecialPage { public 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; + } } /** @@ -132,4 +172,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; + } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index f50fb732..8c546edd 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -388,7 +388,7 @@ class SpecialSearch extends SpecialPage { } } - $out->addHTML( '<div class="visualClear"></div>\n' ); + $out->addHTML( '<div class="visualClear"></div>' ); if ( $prevnext ) { $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } diff --git a/includes/utils/IP.php b/includes/utils/IP.php index 666660aa..13586f3c 100644 --- a/includes/utils/IP.php +++ b/includes/utils/IP.php @@ -132,8 +132,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 @@ -143,8 +144,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 ); @@ -399,8 +408,9 @@ class IP { if ( self::isIPv6( $ip ) ) { $n = 'v6-' . self::IPv6ToRawHex( $ip ); } elseif ( self::isIPv4( $ip ) ) { - // 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 ); |