diff options
author | root <root@luna.archlinux.org> | 2016-05-21 06:38:45 +0000 |
---|---|---|
committer | root <root@luna.archlinux.org> | 2016-05-21 06:38:45 +0000 |
commit | b88e92b7f0ce508c55de8c6ac5159ef544d480be (patch) | |
tree | 86b188507543d2670b7ada1caa8f1139d3d5abe9 | |
parent | a2bbd243c85ea0e425ee3e8c380aba9f254cee61 (diff) | |
parent | 7bf2eb8ba09b54cec804446ea39a3e658773fac9 (diff) |
Merge branch 'master' of https://git.archlinux.org/vhosts/wiki.archlinux.org
44 files changed, 329 insertions, 164 deletions
diff --git a/README.mediawiki b/README.mediawiki deleted file mode 120000 index 100b9382..00000000 --- a/README.mediawiki +++ /dev/null @@ -1 +0,0 @@ -README
\ No newline at end of file diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index e617f00b..fd2e5e69 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -1,6 +1,34 @@ Security reminder: If you have PHP's register_globals option set, you must turn it off. MediaWiki will not work with it enabled. +== MediaWiki 1.26.3 == + +This is a maintenance release of the MediaWiki 1.26 branch. + +== Changes since 1.26.2 == +* (T116266) Fixed undefined property notices in DairikiDiff under HHVM. +* (T123166) Fix fatal error when importing pages to titles which cannot be + created, such as invalid titles or titles the user is not allowed to edit. +* (T122056) Old tokens are remaining valid within a new session +* (T127114) Login throttle can be tricked using non-canonicalized usernames +* (T123653) Cross-domain policy regexp is too narrow +* (T123071) Incorrectly identifying http link in a's href attributes, due to + m modifier in regex +* (T129506) MediaWiki:Gadget-popups.js isn't renderable +* (T125283) Users occasionally logged in as different users after + SessionManager deployment +* (T103239) Patrol allows click catching and patrolling of any page +* (T122807) [tracking] Check php crypto primatives +* (T98313) Graphs can leak tokens, leading to CSRF +* (T130947) Diff generation should use PoolCounter +* (T133507) Careless use of $wgExternalLinkTarget is insecure +* (T132874) API action=move is not rate limited +* (T110143) strip markers can be used to get around html attribute escaping in + (many?) parser tags +* (T116030) Increase pbkdf2 parameter strengths +* (T127420) Pbkdf2Password does not check if hash_pbkdf2() succeeded +* (T126685) Globally throttle password attempts + == MediaWiki 1.26.2 == This is a maintenance release of the MediaWiki 1.26 branch. diff --git a/docs/hooks.txt b/docs/hooks.txt index 7671e6e3..4e134e5d 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -2449,6 +2449,12 @@ $context: (IContextSource) The RequestContext the skin is being created for. &$skin: A variable reference you may set a Skin instance or string key on to override the skin that will be used for the context. +'RequestHasSameOriginSecurity': Called to determine if the request is somehow +flagged to lack same-origin security. Return false to indicate the lack. Note +if the "somehow" involves HTTP headers, you'll probably need to make sure +the header is varied on. +WebRequest $request: The request. + 'ResetPasswordExpiration': Allow extensions to set a default password expiration $user: The user having their password expiration reset &$newExpire: The new expiration date diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php index 9eed2763..e2da350b 100644 --- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php +++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php @@ -111,8 +111,11 @@ class SyntaxHighlight_GeSHi { public static function parserHook( $text, $args = array(), $parser ) { global $wgUseTidy; + // Replace strip markers (For e.g. {{#tag:syntaxhighlight|<nowiki>...}}) + $out = $parser->mStripState->unstripNoWiki( $text ); + // Don't trim leading spaces away, just the linefeeds - $out = preg_replace( '/^\n+/', '', rtrim( $text ) ); + $out = preg_replace( '/^\n+/', '', rtrim( $out ) ); // Convert deprecated attributes if ( isset( $args['enclose'] ) ) { diff --git a/extensions/SyntaxHighlight_GeSHi/tests/parserTests.txt b/extensions/SyntaxHighlight_GeSHi/tests/parserTests.txt index c6aaa9a4..5d5038c9 100644 --- a/extensions/SyntaxHighlight_GeSHi/tests/parserTests.txt +++ b/extensions/SyntaxHighlight_GeSHi/tests/parserTests.txt @@ -154,3 +154,12 @@ Text <source lang="javascript" enclose="none">var a;</source>. <p>Text <code class="mw-highlight" dir="ltr"><span class="kd">var</span> <span class="nx">a</span><span class="p">;</span></code>. </p> !! end + +!! test +Enclose with nowiki +!! input +{{#tag:syntaxhighlight|<nowiki>foo</nowiki>|lang="text"|inline=none}} +!! result +<p><code class="mw-highlight" dir="ltr">foo</code> +</p> +!! end diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 61fec6e1..7498a021 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,7 +75,7 @@ $wgConfigRegistry = array( * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.26.2'; +$wgVersion = '1.26.3'; /** * Name of the site. It must be changed in LocalSettings.php @@ -4188,7 +4188,13 @@ $wgDebugTidy = false; $wgRawHtml = false; /** - * Set a default target for external links, e.g. _blank to pop up a new window + * Set a default target for external links, e.g. _blank to pop up a new window. + * + * This will also set the "noreferrer" and "noopener" link rel to prevent the + * attack described at https://mathiasbynens.github.io/rel-noopener/ . + * Some older browsers may not support these link attributes, hence + * setting $wgExternalLinkTarget to _blank may represent a security risk + * to some of your users. */ $wgExternalLinkTarget = false; @@ -4438,9 +4444,9 @@ $wgPasswordConfig = array( ), 'pbkdf2' => array( 'class' => 'Pbkdf2Password', - 'algo' => 'sha256', - 'cost' => '10000', - 'length' => '128', + 'algo' => 'sha512', + 'cost' => '30000', + 'length' => '64', ), ); diff --git a/includes/Defines.php b/includes/Defines.php index d55bbcf8..2f3d64fe 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -304,3 +304,9 @@ define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions define( 'CONTENT_FORMAT_XML', 'application/xml' ); /**@}*/ + +/**@{ + * Max string length for shell invocations; based on binfmts.h + */ +define( 'SHELL_MAX_ARG_STRLEN', '100000'); +/**@}*/ diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 64aa87ec..c4d5b5bc 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2812,6 +2812,14 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), } wfDebug( "wfShellExec: $cmd\n" ); + // Don't try to execute commands that exceed Linux's MAX_ARG_STRLEN. + // Other platforms may be more accomodating, but we don't want to be + // accomodating, because very long commands probably include user + // input. See T129506. + if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) { + throw new Exception( __METHOD__ . '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' ); + } + $desc = array( 0 => array( 'file', 'php://stdin', 'r' ), 1 => array( 'pipe', 'w' ), diff --git a/includes/Import.php b/includes/Import.php index 6a0bfd09..db4a6b2d 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -728,13 +728,14 @@ class WikiImporter { $title = $this->processTitle( $pageInfo['title'], isset( $pageInfo['ns'] ) ? $pageInfo['ns'] : null ); - if ( !$title ) { + // $title is either an array of two titles or false. + if ( is_array( $title ) ) { + $this->pageCallback( $title ); + list( $pageInfo['_title'], $foreignTitle ) = $title; + } else { $badTitle = true; $skip = true; } - - $this->pageCallback( $title ); - list( $pageInfo['_title'], $foreignTitle ) = $title; } if ( $title ) { @@ -750,10 +751,17 @@ class WikiImporter { } } - $this->pageOutCallback( $pageInfo['_title'], $foreignTitle, + // @note $pageInfo is only set if a valid $title is processed above with + // no error. If we have a valid $title, then pageCallback is called + // above, $pageInfo['title'] is set and we do pageOutCallback here. + // If $pageInfo['_title'] is not set, then $foreignTitle is also not + // set since they both come from $title above. + if ( array_key_exists( '_title', $pageInfo ) ) { + $this->pageOutCallback( $pageInfo['_title'], $foreignTitle, $pageInfo['revisionCount'], $pageInfo['successfulRevisionCount'], $pageInfo ); + } } /** diff --git a/includes/Linker.php b/includes/Linker.php index 9b5ff27b..f0fa4a5a 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1073,7 +1073,16 @@ class Linker { if ( !$title ) { $title = $wgTitle; } - $attribs['rel'] = Parser::getExternalLinkRel( $url, $title ); + $newRel = Parser::getExternalLinkRel( $url, $title ); + if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) { + $attribs['rel'] = $newRel; + } elseif( $newRel !== '' ) { + // Merge the rel attributes. + $newRels = explode( ' ', $newRel ); + $oldRels = explode( ' ', $attribs['rel'] ); + $combined = array_unique( array_merge( $newRels, $oldRels ) ); + $attribs['rel'] = implode( ' ', $combined ); + } $link = ''; $success = Hooks::run( 'LinkerMakeExternalLink', array( &$url, &$text, &$link, &$attribs, $linktype ) ); diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 2da2f6ce..a902f367 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -806,9 +806,18 @@ class MediaWiki { $errno = $errstr = null; $info = wfParseUrl( $this->config->get( 'Server' ) ); MediaWiki\suppressWarnings(); + $host = $info['host']; + $port = 80; + if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) { + $host = "tls://" . $host; + $port = 443; + } + if ( isset( $info['port'] ) ) { + $port = $info['port']; + } $sock = fsockopen( - $info['host'], - isset( $info['port'] ) ? $info['port'] : 80, + $host, + $port, $errno, $errstr, // If it takes more than 100ms to connect to ourselves there diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index c6209eeb..dd99361e 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -154,8 +154,8 @@ function wfGzipHandler( $s ) { */ function wfMangleFlashPolicy( $s ) { # Avoid weird excessive memory usage in PCRE on big articles - if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $s ) ) { - return preg_replace( '/\<\s*cross-domain-policy\s*\>/i', '<NOT-cross-domain-policy>', $s ); + if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $s ) ) { + return preg_replace( '/\<(\s*)(cross-domain-policy(?=\s|\>))/i', '<$1NOT-$2', $s ); } else { return $s; } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 69ed8def..324cab34 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -610,20 +610,6 @@ class OutputPage extends ContextSource { * @return array Array of module names */ public function getModuleStyles( $filter = false, $position = null ) { - // T97420 - $resourceLoader = $this->getResourceLoader(); - - foreach ( $this->mModuleStyles as $val ) { - $module = $resourceLoader->getModule( $val ); - - if ( $module instanceof ResourceLoaderModule && $module->isPositionDefault() ) { - $warning = __METHOD__ . ': style module should define its position explicitly: ' . - $val . ' ' . get_class( $module ); - wfDebugLog( 'resourceloader', $warning ); - wfLogWarning( $warning ); - } - } - return $this->getModules( $filter, $position, 'mModuleStyles' ); } @@ -2044,6 +2030,11 @@ class OutputPage extends ContextSource { * @return string */ public function getVaryHeader() { + // If we vary on cookies, let's make sure it's always included here too. + if ( $this->getCacheVaryCookies() ) { + $this->addVaryHeader( 'Cookie' ); + } + return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ); } @@ -3074,10 +3065,6 @@ class OutputPage extends ContextSource { ResourceLoaderModule::TYPE_SCRIPTS ); - $links[] = $this->makeResourceLoaderLink( $this->getModuleStyles( true, 'bottom' ), - ResourceLoaderModule::TYPE_STYLES - ); - // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the bottom $modules = $this->getModules( true, 'bottom' ); @@ -3140,9 +3127,6 @@ class OutputPage extends ContextSource { * @return string */ function getBottomScripts() { - // In case the skin wants to add bottom CSS - $this->getSkin()->setupSkinUserCss( $this ); - return $this->getScriptsForBottomQueue(); } @@ -3665,7 +3649,7 @@ class OutputPage extends ContextSource { $otherTags = array(); // Tags to append after the normal <link> tags $resourceLoader = $this->getResourceLoader(); - $moduleStyles = $this->getModuleStyles( true, 'top' ); + $moduleStyles = $this->getModuleStyles(); // Per-site custom styles $moduleStyles[] = 'site'; diff --git a/includes/User.php b/includes/User.php index 199dd1dc..f6df7e03 100644 --- a/includes/User.php +++ b/includes/User.php @@ -3630,11 +3630,14 @@ class User implements IDBAccessObject { $this->clearInstanceCache( 'defaults' ); $this->getRequest()->setSessionData( 'wsUserID', 0 ); + $this->getRequest()->setSessionData( 'wsEditToken', null ); $this->clearCookie( 'UserID' ); $this->clearCookie( 'Token' ); $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) ); + wfResetSessionID(); + // Remember when user logged out, to prevent seeing cached pages $this->setCookie( 'LoggedOut', time(), time() + 86400 ); } diff --git a/includes/WebStart.php b/includes/WebStart.php index f5a4f93b..e75e97fd 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -40,6 +40,9 @@ if ( function_exists( 'get_magic_quotes_gpc' ) && get_magic_quotes_gpc() ) { . 'for help on how to disable magic quotes.' ); } +if ( ini_get( 'mbstring.func_overload' ) ) { + die( 'MediaWiki does not support installations where mbstring.func_overload is non-zero.' ); +} # bug 15461: Make IE8 turn off content sniffing. Everybody else should ignore this # We're adding it here so that it's *always* set, even for alternate entry diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index b71b0e9e..6b99258f 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -90,6 +90,12 @@ class RawAction extends FormlessAction { } } + // Set standard Vary headers so cache varies on cookies and such (T125283) + $response->header( $this->getOutput()->getVaryHeader() ); + if ( $config->get( 'UseXVO' ) ) { + $response->header( $this->getOutput()->getXVO() ); + } + $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' ); // Output may contain user-specific data; // vary generated content for open sessions on private wikis diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index d53797bc..4f40499c 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -421,7 +421,13 @@ abstract class ApiBase extends ContextSource { * @return bool */ public function lacksSameOriginSecurity() { - return $this->getMain()->getRequest()->getVal( 'callback' ) !== null; + // Main module has this method overridden + // Safety - avoid infinite loop: + if ( $this->isMain() ) { + ApiBase::dieDebug( __METHOD__, 'base method was called on main module.' ); + } + + return $this->getMain()->lacksSameOriginSecurity(); } /** diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index be1b12c3..baba5b2d 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -102,9 +102,9 @@ class ApiFormatJson extends ApiFormatBase { // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API, so we need to // work around it. - if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $json ) ) { + if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $json ) ) { $json = preg_replace( - '/\<(\s*cross-domain-policy\s*)\>/i', '\\u003C$1\\u003E', $json + '/\<(\s*cross-domain-policy(?=\s|\>))/i', '\\u003C$1', $json ); } diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index 6420a5b5..643379c7 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -65,7 +65,7 @@ class ApiFormatPhp extends ApiFormatBase { // just be broken in a useful manner. if ( $this->getConfig()->get( 'MangleFlashPolicy' ) && in_array( 'wfOutputHandler', ob_list_handlers(), true ) && - preg_match( '/\<\s*cross-domain-policy\s*\>/i', $text ) + preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $text ) ) { $this->dieUsage( 'This response cannot be represented using format=php. ' . diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index d943c86b..1f0aebb6 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -145,6 +145,9 @@ class ApiMain extends ApiBase { private $mCacheControl = array(); private $mParamsUsed = array(); + /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */ + private $lacksSameOriginSecurity = null; + /** * Constructs an instance of ApiMain that utilizes the module and format specified by $request. * @@ -243,6 +246,36 @@ class ApiMain extends ApiBase { } /** + * Get the security flag for the current request + * @return bool + */ + public function lacksSameOriginSecurity() { + if ( $this->lacksSameOriginSecurity !== null ) { + return $this->lacksSameOriginSecurity; + } + + $request = $this->getRequest(); + + // JSONP mode + if ( $request->getVal( 'callback' ) !== null ) { + $this->lacksSameOriginSecurity = true; + return true; + } + + // Header to be used from XMLHTTPRequest when the request might + // otherwise be used for XSS. + if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) { + $this->lacksSameOriginSecurity = true; + return true; + } + + // Allow extensions to override. + $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', array( $request ) ); + return $this->lacksSameOriginSecurity; + } + + + /** * Get the ApiErrorFormatter object associated with current request * @return ApiErrorFormatter */ @@ -717,6 +750,8 @@ class ApiMain extends ApiBase { $response = $this->getRequest()->response(); $out = $this->getOutput(); + $out->addVaryHeader( 'Treat-as-Untrusted' ); + $config = $this->getConfig(); if ( $config->get( 'VaryOnXFP' ) ) { diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index aca43784..dc50594c 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -72,6 +72,11 @@ class ApiMove extends ApiBase { } } + // Rate limit + if ( $user->pingLimiter( 'move' ) ) { + $this->dieUsageMsg( 'actionthrottledtext' ); + } + // Move the page $toTitleExists = $toTitle->exists(); $status = $this->movePage( $fromTitle, $toTitle, $params['reason'], !$params['noredirect'] ); diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 56a5b2cf..d5143689 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -486,6 +486,9 @@ class DatabasePostgres extends DatabaseBase { if ( function_exists( 'mb_convert_encoding' ) ) { $sql = mb_convert_encoding( $sql, 'UTF-8' ); } + while ( $res = pg_get_result( $this->mConn ) ) { + pg_free_result( $res ); + } $this->mTransactionState->check(); if ( pg_send_query( $this->mConn, $sql ) === false ) { throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php index d327433f..7bdc6543 100644 --- a/includes/diff/DairikiDiff.php +++ b/includes/diff/DairikiDiff.php @@ -296,9 +296,9 @@ class DiffEngine { $this->xchanged = $this->ychanged = array(); $this->xv = $this->yv = array(); $this->xind = $this->yind = array(); - unset( $this->seq ); - unset( $this->in_seq ); - unset( $this->lcs ); + $this->seq = array(); + $this->in_seq = array(); + $this->lcs = 0; // Skip leading common lines. for ( $skip = 0; $skip < $n_from && $skip < $n_to; $skip++ ) { diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index c138eec2..fa1cd79d 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -511,7 +511,7 @@ class DifferenceEngine extends ContextSource { $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( $this->mNewPage, $this->msg( 'markaspatrolleddiff' )->escaped(), - array(), + array( 'class' => 'mw-patrollink' ), array( 'action' => 'markpatrolled', 'rcid' => $rcid, @@ -823,6 +823,35 @@ class DifferenceEngine extends ContextSource { * @return bool|string */ public function generateTextDiffBody( $otext, $ntext ) { + $self = $this; + $diff = function() use ( $self, $otext, $ntext ) { + return $self->textDiff( $otext, $ntext ); + }; + + $error = function( $status ) { + throw new FatalError( $status->getWikiText() ); + }; + + // Use PoolCounter if the diff looks like it can be expensive + if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) { + $work = new PoolCounterWorkViaCallback( 'diff', + md5( $otext ) . md5( $ntext ), + array( 'doWork' => $diff, 'error' => $error ) + ); + return $work->execute(); + } + + return $diff(); + } + + /** + * Generates diff, to be wrapped internally in a logging/instrumentation + * + * @param string $otext Old text, must be already segmented + * @param string $ntext New text, must be already segmented + * @return bool|string + */ + public function textDiff( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; $otext = str_replace( "\r\n", "\n", $otext ); diff --git a/includes/page/Article.php b/includes/page/Article.php index 56b9520a..97412dc7 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -1168,7 +1168,7 @@ class Article implements Page { $link = Linker::linkKnown( $this->getTitle(), wfMessage( 'markaspatrolledtext' )->escaped(), - array(), + array( 'class' => 'mw-patrollink' ), array( 'action' => 'markpatrolled', 'rcid' => $rcid, diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index 9755ea93..3f4f54a3 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -56,9 +56,14 @@ class CoreTagHooks { $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' ); $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); - return Xml::openElement( 'pre', $attribs ) . - Xml::escapeTagsOnly( $content ) . - '</pre>'; + // We need to let both '"' and '&' through, + // for strip markers and entities respectively. + $content = str_replace( + array( '>', '<' ), + array( '>', '<' ), + $content + ); + return Html::rawElement( 'pre', $attribs, $content ); } /** @@ -98,8 +103,17 @@ class CoreTagHooks { * @return array */ public static function nowiki( $content, $attributes, $parser ) { - $content = strtr( $content, array( '-{' => '-{', '}-' => '}-' ) ); - return array( Xml::escapeTagsOnly( $content ), 'markerType' => 'nowiki' ); + $content = strtr( $content, array( + // lang converter + '-{' => '-{', + '}-' => '}-', + // html tags + '<' => '<', + '>' => '>' + // Note: Both '"' and '&' are not converted. + // This allows strip markers and entities through. + ) ); + return array( $content, 'markerType' => 'nowiki' ); } /** diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index c07a08ac..12953167 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -129,9 +129,14 @@ class Parser { * * Must not consist of all title characters, or else it will change * the behavior of <nowiki> in a link. + * + * Must have a character that needs escaping in attributes, otherwise + * someone could put a strip marker in an attribute, to get around + * escaping quote marks, and break out of the attribute. Thus we add + * `'". */ - const MARKER_SUFFIX = "-QINU\x7f"; - const MARKER_PREFIX = "\x7fUNIQ-"; + const MARKER_SUFFIX = "-QINU`\"'\x7f"; + const MARKER_PREFIX = "\x7f'\"`UNIQ-"; # Markers used for wrapping the table of contents const TOC_START = '<mw:toc>'; @@ -1862,11 +1867,22 @@ class Parser { */ public function getExternalLinkAttribs( $url = false ) { $attribs = array(); - $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle ); - - if ( $this->mOptions->getExternalLinkTarget() ) { - $attribs['target'] = $this->mOptions->getExternalLinkTarget(); + $rel = self::getExternalLinkRel( $url, $this->mTitle ); + + $target = $this->mOptions->getExternalLinkTarget(); + if ( $target ) { + $attribs['target'] = $target; + if ( !in_array( $target, array( '_self', '_parent', '_top' ) ) ) { + // T133507. New windows can navigate parent cross-origin. + // Including noreferrer due to lacking browser + // support of noopener. Eventually noreferrer should be removed. + if ( $rel !== '' ) { + $rel .= ' '; + } + $rel .= 'noreferrer noopener'; + } } + $attribs['rel'] = $rel; return $attribs; } diff --git a/includes/password/MWOldPassword.php b/includes/password/MWOldPassword.php index afa5cacc..43c13553 100644 --- a/includes/password/MWOldPassword.php +++ b/includes/password/MWOldPassword.php @@ -44,5 +44,9 @@ class MWOldPassword extends ParameterizedPassword { $this->args = array(); $this->hash = md5( $plaintext ); } + + if ( !is_string( $this->hash ) || strlen( $this->hash ) < 32 ) { + throw new PasswordError( 'Error when hashing password.' ); + } } } diff --git a/includes/password/MWSaltedPassword.php b/includes/password/MWSaltedPassword.php index 6c6895a2..40d2b6a1 100644 --- a/includes/password/MWSaltedPassword.php +++ b/includes/password/MWSaltedPassword.php @@ -42,5 +42,9 @@ class MWSaltedPassword extends ParameterizedPassword { } $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) ); + + if ( !is_string( $this->hash ) || strlen( $this->hash ) < 32 ) { + throw new PasswordError( 'Error when hashing password.' ); + } } } diff --git a/includes/password/Pbkdf2Password.php b/includes/password/Pbkdf2Password.php index 080e3b0d..808fd8a6 100644 --- a/includes/password/Pbkdf2Password.php +++ b/includes/password/Pbkdf2Password.php @@ -55,8 +55,15 @@ class Pbkdf2Password extends ParameterizedPassword { (int)$this->params['length'], true ); + if ( !is_string( $hash ) ) { + throw new PasswordError( 'Error when hashing password.' ); + } } else { - $hashLen = strlen( hash( $this->params['algo'], '', true ) ); + $hashLenHash = hash( $this->params['algo'], '', true ); + if ( !is_string( $hashLenHash ) ) { + throw new PasswordError( 'Error when hashing password.' ); + } + $hashLen = strlen( $hashLenHash ); $blockCount = ceil( $this->params['length'] / $hashLen ); $hash = ''; diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 7fbc1cb4..8060f6e1 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -279,7 +279,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; // Single strings case 'position': - $this->isPositionDefined = true; case 'group': case 'skipFunction': $this->{$member} = (string)$option; diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php index 8de87f2e..e2da28bb 100644 --- a/includes/resourceloader/ResourceLoaderImageModule.php +++ b/includes/resourceloader/ResourceLoaderImageModule.php @@ -184,7 +184,6 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { break; case 'position': - $this->isPositionDefined = true; case 'prefix': case 'selectorWithoutVariant': case 'selectorWithVariant': @@ -456,9 +455,4 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $this->loadFromDefinition(); return $this->position; } - - public function isPositionDefault() { - $this->loadFromDefinition(); - return parent::isPositionDefault(); - } } diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 1d3ffb55..9b6702aa 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -67,10 +67,6 @@ abstract class ResourceLoaderModule { // In-object cache for module content protected $contents = array(); - // Whether the position returned by getPosition() is defined in the module configuration - // and not a default value - protected $isPositionDefined = false; - /** * @var Config */ @@ -292,19 +288,6 @@ abstract class ResourceLoaderModule { } /** - * Whether the position returned by getPosition() is a default value or comes from the module - * definition. This method is meant to be short-lived, and is only useful until classes added - * via addModuleStyles with a default value define an explicit position. See getModuleStyles() - * in OutputPage for the related migration warning. - * - * @return bool - * @since 1.26 - */ - public function isPositionDefault() { - return !$this->isPositionDefined; - } - - /** * Whether this module's JS expects to work without the client-side ResourceLoader module. * Returning true from this function will prevent mw.loader.state() call from being * appended to the bottom of the script. diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 0023de27..693bcee4 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -72,8 +72,6 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { foreach ( $options as $member => $option ) { switch ( $member ) { case 'position': - $this->isPositionDefined = true; - // Don't break since we need the member set as well case 'styles': case 'scripts': case 'group': diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 21f1194f..2d6737bd 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -628,7 +628,7 @@ class LoginForm extends SpecialPage { "allowed account creation w/o throttle\n" ); } else { if ( ( $wgAccountCreationThrottle && $currentUser->isPingLimitable() ) ) { - $key = wfMemcKey( 'acctcreate', 'ip', $ip ); + $key = wfGlobalCacheKey( 'acctcreate', 'ip', $ip ); $value = $wgMemc->get( $key ); if ( !$value ) { $wgMemc->set( $key, 0, 86400 ); @@ -862,11 +862,12 @@ class LoginForm extends SpecialPage { */ public static function incLoginThrottle( $username ) { global $wgPasswordAttemptThrottle, $wgMemc, $wgRequest; - $username = trim( $username ); // sanity + $canUsername = User::getCanonicalName( $username, 'usable' ); + $username = $canUsername !== false ? $canUsername : $username; $throttleCount = 0; if ( is_array( $wgPasswordAttemptThrottle ) ) { - $throttleKey = wfMemcKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); + $throttleKey = wfGlobalCacheKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); $count = $wgPasswordAttemptThrottle['count']; $period = $wgPasswordAttemptThrottle['seconds']; @@ -890,9 +891,10 @@ class LoginForm extends SpecialPage { */ public static function clearLoginThrottle( $username ) { global $wgMemc, $wgRequest; - $username = trim( $username ); // sanity + $canUsername = User::getCanonicalName( $username, 'usable' ); + $username = $canUsername !== false ? $canUsername : $username; - $throttleKey = wfMemcKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); + $throttleKey = wfGlobalCacheKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); $wgMemc->delete( $throttleKey ); } @@ -1608,7 +1610,8 @@ class LoginForm extends SpecialPage { if ( $wgSecureLogin && !$this->mStickHTTPS ) { $wgCookieSecure = false; } - + // Always make sure edit token is regenerated. (T114419) + $this->getRequest()->setSessionData( 'wsEditToken', null ); wfResetSessionID(); } diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index e9e1f658..02192904 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -1399,7 +1399,7 @@ abstract class UploadBase { && strpos( $value, '#' ) !== 0 ) { if ( !( $strippedElement === 'a' - && preg_match( '!^https?://!im', $value ) ) + && preg_match( '!^https?://!i', $value ) ) ) { wfDebug( __METHOD__ . ": Found href attribute <$strippedElement " . "'$attrib'='$value' in uploaded file.\n" ); diff --git a/resources/Resources.php b/resources/Resources.php index 6a22af66..6626f05a 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2031,6 +2031,8 @@ return array( 'dependencies' => array( 'oojs-ui', 'mediawiki.api', + 'mediawiki.ForeignApi', + 'mediawiki.Title', ), 'targets' => array( 'desktop', 'mobile' ), ), diff --git a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js index f9b0d356..e0307b72 100644 --- a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js +++ b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js @@ -12,7 +12,7 @@ return; } $( function () { - var $patrolLinks = $( '.patrollink a' ); + var $patrolLinks = $( '.patrollink a.mw-patrollink' ); $patrolLinks.on( 'click', function ( e ) { var $spinner, href, rcid, apiRequest; diff --git a/tests/browser/README.mediawiki b/tests/browser/README.mediawiki deleted file mode 100644 index 22657627..00000000 --- a/tests/browser/README.mediawiki +++ /dev/null @@ -1,64 +0,0 @@ -Purpose: - -The purpose of these tests is to validate that a newly installed (or updated, or hacked, or whatever) mediawiki instance presents to the user a set of expected features, regardless of what language the wiki is in, or where it is installed, or what extensions it might have. - -The tests are based on the basic definition of a wiki, a website where anyone - -* can read a page -* can create a page -* can edit a page -* can link one page to another page - -Install: - -Ruby 1.9.3 or higher is required -Firefox browser is required -:: - cd /tests/browser - gem update --system - gem install bundler - bundle install - -Run the tests: - -Edit the environment_variables file with appropriate values for your wiki -$source environment_variables (example shown in bash shell) - -bundle exec cucumber features/ - -Note that the acceptance tests will create three pages in your wiki entitled "Editing Test Page", "Link Source Test Page", and "Link Target Test Page". These pages may be deleted at any time. If you wish to re-run the tests at any time, these test pages will be re-created or reset to their original contents at the time that the tests run. - -For more information about running Selenium tests please see -https://github.com/wikimedia/mediawiki-selenium - -Details: - -create_account.feature -* Checks three different ways to arrive on page allowing the user to create an account - -create_and_follow_wiki_link.feature: -* uses the mediawiki API to create a link target page -* uses the mediawiki API to create a link source page -* navigates a browser to the link source page -* clicks the link in that page to the link target page -* validates that the browser has in fact followed the link to the target page correctly - -edit_page.feature: -* uses the mediawiki API to create an editable page on the wiki -* navigates a browser to the page -* clicks the Edit button to invoke the basic editor -* edits the page with a particular string containing a static part and also a quasi-unique random part -* saves the edited page -* checks that the saved page contains the particular string with which the page was edited - -main_page.feature: -* navigates a browser to the default landing page of the wiki -* checks for the View History link on the landing page -* checks for the full set of of sidebar links that should exist on every mediawiki wiki - -view_history.feature -* similar to edit_page.feature but checks for an older version of the edited page - -Notes: - -Tested on beta labs hewiki, dewiki, enwiki, and on a local installation of mediawiki
\ No newline at end of file diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt index c8c63f39..67da1f0a 100644 --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@ -2263,6 +2263,15 @@ Entities inside <pre> !! end !! test +<nowiki> inside of #tag:pre +!! wikitext +{{#tag:pre|Foo <nowiki>→bar</nowiki>}} +!! html +<pre>Foo →bar</pre> + +!! end + +!! test <nowiki> and <pre> preference (first one wins) !! wikitext <pre> @@ -12863,7 +12872,7 @@ Image with link parameter, wgExternalLinkTarget !! config wgExternalLinkTarget='foobar' !! html -<p><a href="http://example.com/" target="foobar" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" target="foobar" rel="nofollow noreferrer noopener"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -12896,7 +12905,7 @@ Image with link parameter, wgExternalLinkTarget, unnamed parameter !! config wgExternalLinkTarget='foobar' !! html -<p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> +<p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow noreferrer noopener"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php index 94b741dc..a2bc7aed 100644 --- a/tests/phpunit/includes/api/ApiMainTest.php +++ b/tests/phpunit/includes/api/ApiMainTest.php @@ -248,4 +248,31 @@ class ApiMainTest extends ApiTestCase { ); } + /** + * @covers ApiMain::lacksSameOriginSecurity + */ + public function testLacksSameOriginSecurity() { + // Basic test + $main = new ApiMain( new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ) ) ); + $this->assertFalse( $main->lacksSameOriginSecurity(), 'Basic test, should have security' ); + + // JSONp + $main = new ApiMain( + new FauxRequest( array( 'action' => 'query', 'format' => 'xml', 'callback' => 'foo' ) ) + ); + $this->assertTrue( $main->lacksSameOriginSecurity(), 'JSONp, should lack security' ); + + // Header + $request = new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ) ); + $request->setHeader( 'TrEaT-As-UnTrUsTeD', '' ); // With falsey value! + $main = new ApiMain( $request ); + $this->assertTrue( $main->lacksSameOriginSecurity(), 'Header supplied, should lack security' ); + + // Hook + $this->mergeMwGlobalArrayValue( 'wgHooks', array( + 'RequestHasSameOriginSecurity' => array( function () { return false; } ) + ) ); + $main = new ApiMain( new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ) ) ); + $this->assertTrue( $main->lacksSameOriginSecurity(), 'Hook, should lack security' ); + } } diff --git a/tests/phpunit/includes/api/format/ApiFormatJsonTest.php b/tests/phpunit/includes/api/format/ApiFormatJsonTest.php index 3dfcaf0f..8d599b08 100644 --- a/tests/phpunit/includes/api/format/ApiFormatJsonTest.php +++ b/tests/phpunit/includes/api/format/ApiFormatJsonTest.php @@ -61,7 +61,7 @@ class ApiFormatJsonTest extends ApiFormatTestBase { array( array( 1 ), '/**/myCallback([1])', array( 'callback' => 'myCallback' ) ), // Cross-domain mangling - array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy \u003E"]' ), + array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy >"]' ), ) ), self::addFormatVersion( 2, array( // Basic types @@ -102,7 +102,7 @@ class ApiFormatJsonTest extends ApiFormatTestBase { array( array( 1 ), '/**/myCallback([1])', array( 'callback' => 'myCallback' ) ), // Cross-domain mangling - array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy \u003E"]' ), + array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy >"]' ), ) ) ); } diff --git a/tests/phpunit/includes/upload/UploadBaseTest.php b/tests/phpunit/includes/upload/UploadBaseTest.php index 9441b77f..a3f8ae48 100644 --- a/tests/phpunit/includes/upload/UploadBaseTest.php +++ b/tests/phpunit/includes/upload/UploadBaseTest.php @@ -374,6 +374,12 @@ class UploadBaseTest extends MediaWikiTestCase { false, 'SVG with external entity' ), + array( + "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"> <g> <a xlink:href=\"javascript:alert('1 https://google.com')\"> <rect width=\"300\" height=\"100\" style=\"fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,2)\" /> </a> </g> </svg>", + true, + true, + 'SVG with javascript <a> link with newline (T122653)' + ), // Test good, but strange files that we want to allow array( diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js index 01f96252..53bff763 100644 --- a/tests/qunit/data/testrunner.js +++ b/tests/qunit/data/testrunner.js @@ -27,8 +27,6 @@ // and assuming failure. QUnit.config.testTimeout = 30 * 1000; - QUnit.config.requireExpects = true; - // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode. QUnit.config.urlConfig.push( { id: 'debug', |