From 1c7889446d56045dd196a9ebbed634430d90efc7 Mon Sep 17 00:00:00 2001 From: Brian Wolff Date: Mon, 6 Feb 2017 03:00:39 +0000 Subject: [PATCH] SECURITY: Disable tag on system messages despite $wgRawHtml = true; System messages may take parameters from untrusted sources. This may include taking parameters from urls given by unauthenticated users even if the wiki is a read-only wiki. Allowing tags in such a context seems like an accident waiting to happen. Bug: T156184 Change-Id: I661f482986d319cf41da1d3e7b20a0f028a42e90 --- includes/OutputPage.php | 3 +++ includes/cache/MessageCache.php | 5 ++++ includes/parser/CoreTagHooks.php | 17 +++++++++++-- includes/parser/ParserOptions.php | 33 ++++++++++++++++++++++++++ languages/i18n/en.json | 3 ++- languages/i18n/qqq.json | 3 ++- tests/phpunit/includes/MessageTest.php | 17 +++++++++++++ 7 files changed, 77 insertions(+), 4 deletions(-) diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 9ecfa23e2a..d3e137330d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1568,6 +1568,7 @@ class OutputPage extends ContextSource { // been changed somehow, and keep it if so. $anonPO = ParserOptions::newFromAnon(); $anonPO->setEditSection( false ); + $anonPO->setAllowUnsafeRawHtml( false ); if ( !$options->matches( $anonPO ) ) { wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) ); $options->isBogus = false; @@ -1581,6 +1582,7 @@ class OutputPage extends ContextSource { // either. $po = ParserOptions::newFromAnon(); $po->setEditSection( false ); + $po->setAllowUnsafeRawHtml( false ); $po->isBogus = true; if ( $options !== null ) { $this->mParserOptions = empty( $options->isBogus ) ? $options : null; @@ -1590,6 +1592,7 @@ class OutputPage extends ContextSource { $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() ); $this->mParserOptions->setEditSection( false ); + $this->mParserOptions->setAllowUnsafeRawHtml( false ); } if ( $options !== null && !empty( $options->isBogus ) ) { diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 7cd489a620..70e1d9a75d 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -191,11 +191,16 @@ class MessageCache { // either. $po = ParserOptions::newFromAnon(); $po->setEditSection( false ); + $po->setAllowUnsafeRawHtml( false ); return $po; } $this->mParserOptions = new ParserOptions; $this->mParserOptions->setEditSection( false ); + // Messages may take parameters that could come + // from malicious sources. As a precaution, disable + // the parser tag when parsing messages. + $this->mParserOptions->setAllowUnsafeRawHtml( false ); } return $this->mParserOptions; diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index c943b7c986..438603a841 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -79,12 +79,25 @@ class CoreTagHooks { * @param array $attributes * @param Parser $parser * @throws MWException - * @return array + * @return array|string Output of tag hook */ public static function html( $content, $attributes, $parser ) { global $wgRawHtml; if ( $wgRawHtml ) { - return [ $content, 'markerType' => 'nowiki' ]; + if ( $parser->getOptions()->getAllowUnsafeRawHtml() ) { + return [ $content, 'markerType' => 'nowiki' ]; + } else { + // In a system message where raw html is + // not allowed (but it is allowed in other + // contexts). + return Html::rawElement( + 'span', + [ 'class' => 'error' ], + // Using ->text() not ->parse() as + // a paranoia measure against a loop. + wfMessage( 'rawhtml-notallowed' )->escaped() + ); + } } else { throw new MWException( ' extension tag encountered unexpectedly' ); } diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 7be82818e7..2cdd8c7075 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -243,6 +243,21 @@ class ParserOptions { */ private $redirectTarget = null; + /** + * If the wiki is configured to allow raw html ($wgRawHtml = true) + * is it allowed in the specific case of parsing this page. + * + * This is meant to disable unsafe parser tags in cases where + * a malicious user may control the input to the parser. + * + * @note This is expected to be true for normal pages even if the + * wiki has $wgRawHtml disabled in general. The setting only + * signifies that raw html would be unsafe in the current context + * provided that raw html is allowed at all. + * @var boolean + */ + private $allowUnsafeRawHtml = true; + public function getInterwikiMagic() { return $this->mInterwikiMagic; } @@ -457,6 +472,15 @@ class ParserOptions { public function getMagicRFCLinks() { return $this->mMagicRFCLinks; } + + /** + * @since 1.29 + * @return bool + */ + public function getAllowUnsafeRawHtml() { + return $this->allowUnsafeRawHtml; + } + public function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } @@ -596,6 +620,15 @@ class ParserOptions { return wfSetVar( $this->mIsPrintable, $x ); } + /** + * @param bool|null Value to set or null to get current value + * @return bool Current value for allowUnsafeRawHtml + * @since 1.29 + */ + public function setAllowUnsafeRawHtml( $x ) { + return wfSetVar( $this->allowUnsafeRawHtml, $x ); + } + /** * Set the redirect target. * diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 0c8bf210c8..f9fbfd07a8 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4283,5 +4283,6 @@ "restrictionsfield-label": "Allowed IP ranges:", "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use:
0.0.0.0/0\n::/0
", "revid": "revision $1", - "pageid": "page ID $1" + "pageid": "page ID $1", + "rawhtml-notallowed": "<html> tags cannot be used outside of normal pages." } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 2951a4cd38..75be7aac92 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4470,5 +4470,6 @@ "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).", "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}", - "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number." + "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.", + "rawhtml-notallowed": "Error message given when $wgRawHtml = true; is set and a user uses an <html> tag in a system message or somewhere other than a normal page." } diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index 424218e6e1..58087c1826 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -1,4 +1,5 @@ assertSame( 'example &', $msg->escaped() ); } + public function testRawHtmlInMsg() { + global $wgParserConf; + $this->setMwGlobals( 'wgRawHtml', true ); + // We have to reset the core hook registration. + // to register the html hook + MessageCache::destroyInstance(); + $this->setMwGlobals( 'wgParser', + ObjectFactory::constructClassInstance( $wgParserConf['class'], [ $wgParserConf ] ) + ); + + $msg = new RawMessage( '' ); + $txt = '<html> tags cannot be' . + ' used outside of normal pages.'; + $this->assertSame( $txt, $msg->parse() ); + } + /** * @covers Message::params * @covers Message::toString -- 2.20.1