Reedy has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/391454 )
Change subject: SECURITY: XSS in langconverter when regex hits pcre.backtrack_limit ...................................................................... SECURITY: XSS in langconverter when regex hits pcre.backtrack_limit Adjust regexes for what not to convert to avoid backtracking by preferring possesive quantifiers Add check that we really have matched to the end of the string, and log error if the regex hits some sort of error preventing the entire string from being matched. Should the regex not match to the end, then language conversion is disabled for the string. Bug: T124404 Change-Id: I4f0c171c7da804e9c1508ef1f59556665a318f6a --- M languages/LanguageConverter.php M tests/phpunit/languages/LanguageConverterTest.php 2 files changed, 61 insertions(+), 15 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/54/391454/1 diff --git a/languages/LanguageConverter.php b/languages/LanguageConverter.php index 67c0ca7..00bc02d 100644 --- a/languages/LanguageConverter.php +++ b/languages/LanguageConverter.php @@ -20,6 +20,8 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Logger\LoggerFactory; + /** * Base class for language conversion. * @ingroup Language @@ -353,24 +355,30 @@ } /* we convert everything except: - * 1. HTML markups (anything between < and >) - * 2. HTML entities - * 3. placeholders created by the parser - */ - $marker = '|' . Parser::MARKER_PREFIX . '[\-a-zA-Z0-9]+'; + 1. HTML markups (anything between < and >) + 2. HTML entities + 3. placeholders created by the parser + IMPORTANT: Beware of failure from pcre.backtrack_limit (T124404). + Minimize use of backtracking where possible. + */ + $marker = '|' . Parser::MARKER_PREFIX . '[^\x7f]++\x7f'; // this one is needed when the text is inside an HTML markup - $htmlfix = '|<[^>]+$|^[^<>]*>'; + $htmlfix = '|<[^>\004]++(?=\004$)|^[^<>]*+>'; + + // Optimize for the common case where these tags have + // few or no children. Thus try and possesively get as much as + // possible, and only engage in backtracking when we hit a '<'. // disable convert to variants between <code> tags - $codefix = '<code>.+?<\/code>|'; + $codefix = '<code>[^<]*+(?:(?:(?!<\/code>).)[^<]*+)*+<\/code>|'; // disable conversion of <script> tags - $scriptfix = '<script.*?>.*?<\/script>|'; + $scriptfix = '<script[^>]*+>[^<]*+(?:(?:(?!<\/script>).)[^<]*+)*+<\/script>|'; // disable conversion of <pre> tags - $prefix = '<pre.*?>.*?<\/pre>|'; + $prefix = '<pre[^>]*+>[^<]*+(?:(?:(?!<\/pre>).)[^<]*+)*+<\/pre>|'; $reg = '/' . $codefix . $scriptfix . $prefix . - '<[^>]+>|&[a-zA-Z#][a-z0-9]+;' . $marker . $htmlfix . '/s'; + '<[^>]++>|&[a-zA-Z#][a-z0-9]++;' . $marker . $htmlfix . '|\004$/s'; $startPos = 0; $sourceBlob = ''; $literalBlob = ''; @@ -381,15 +389,34 @@ $markupMatches = null; $elementMatches = null; + + // We add a marker (\004) at the end of text, to ensure we always match the + // entire text (Otherwise, pcre.backtrack_limit might cause silent failure) while ( $startPos < strlen( $text ) ) { - if ( preg_match( $reg, $text, $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) { + if ( preg_match( $reg, $text . "\004", $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) { $elementPos = $markupMatches[0][1]; $element = $markupMatches[0][0]; + if ( $element === "\004" ) { + // We hit the end. + $elementPos = strlen( $text ); + $element = ''; + } } else { - $elementPos = strlen( $text ); - $element = ''; + // If we hit here, then Language Converter could be tricked + // into doing an XSS, so we refuse to translate. + // If non-crazy input manages to reach this code path, + // we should consider it a bug. + $log = LoggerFactory::getInstance( 'languageconverter' ); + $log->error( "Hit pcre.backtrack_limit in " . __METHOD__ + . ". Disabling language conversion for this page.", + array( + "method" => __METHOD__, + "variant" => $toVariant, + "startOfText" => substr( $text, 0, 500 ) + ) + ); + return $text; } - // Queue the part before the markup for translation in a batch $sourceBlob .= substr( $text, $startPos, $elementPos - $startPos ) . "\000"; @@ -398,7 +425,7 @@ // Translate any alt or title attributes inside the matched element if ( $element !== '' - && preg_match( '/^(<[^>\s]*)\s([^>]*)(.*)$/', $element, $elementMatches ) + && preg_match( '/^(<[^>\s]*+)\s([^>]*+)(.*+)$/', $element, $elementMatches ) ) { $attrs = Sanitizer::decodeTagAttributes( $elementMatches[2] ); $changed = false; diff --git a/tests/phpunit/languages/LanguageConverterTest.php b/tests/phpunit/languages/LanguageConverterTest.php index 81184aa..331368e 100644 --- a/tests/phpunit/languages/LanguageConverterTest.php +++ b/tests/phpunit/languages/LanguageConverterTest.php @@ -157,6 +157,25 @@ $wgRequest->setVal( 'variant', null ); $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); } + + /** + * Test exhausting pcre.backtrack_limit + */ + public function testAutoConvertT124404() { + $testString = ''; + for ( $i = 0; $i < 1000; $i++ ) { + $testString .= 'xxx xxx xxx'; + } + $testString .= "\n<big id='в'></big>"; + $old = ini_set('pcre.backtrack_limit', 200 ); + $result = $this->lc->autoConvert( $testString, 'tg-latn' ); + ini_set( 'pcre.backtrack_limit', $old ); + // The в in the id attribute should not get converted to a v + $this->assertFalse( + strpos( $result, 'v' ), + "в converted to v despite being in attribue" + ); + } } /** -- To view, visit https://gerrit.wikimedia.org/r/391454 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I4f0c171c7da804e9c1508ef1f59556665a318f6a Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Reedy <re...@wikimedia.org> Gerrit-Reviewer: Brian Wolff <bawolff...@gmail.com> Gerrit-Reviewer: Reedy <re...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits