X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Factions%2FRawAction.php;h=3fda401bd764f2727cedbf07b17dc05b23570284;hp=d8c8bc3213eee560d4136e11d1e4429bc347d05c;hb=a5e349c90843353df5e3c961ac78533bf0644897;hpb=dae4c94d893057345f62a3d498fb85c0a54de5a6 diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index d8c8bc3213..3fda401bd7 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -26,6 +26,8 @@ * @file */ +use MediaWiki\Logger\LoggerFactory; + /** * A simple method to retrieve the plain source of an article, * using "action=raw" in the GET request string. @@ -59,20 +61,19 @@ class RawAction extends FormlessAction { return; // Client cache fresh and headers sent, nothing more to do. } - $gen = $request->getVal( 'gen' ); - if ( $gen == 'css' || $gen == 'js' ) { - $this->gen = true; - } - $contentType = $this->getContentType(); $maxage = $request->getInt( 'maxage', $config->get( 'SquidMaxage' ) ); $smaxage = $request->getIntOrNull( 'smaxage' ); if ( $smaxage === null ) { - if ( $contentType == 'text/css' || $contentType == 'text/javascript' ) { - // CSS/JS raw content has its own CDN max age configuration. - // Note: Title::getCdnUrls() includes action=raw for css/js pages, - // so if using the canonical url, this will get HTCP purges. + if ( + $contentType == 'text/css' || + $contentType == 'application/json' || + $contentType == 'text/javascript' + ) { + // CSS/JSON/JS raw content has its own CDN max age configuration. + // Note: Title::getCdnUrls() includes action=raw for css/json/js + // pages, so if using the canonical url, this will get HTCP purges. $smaxage = intval( $config->get( 'ForcedRawSMaxage' ) ); } else { // No CDN cache for anything else @@ -86,7 +87,6 @@ class RawAction extends FormlessAction { $response->header( $this->getOutput()->getKeyHeader() ); } - $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' ); // Output may contain user-specific data; // vary generated content for open sessions on private wikis $privateCache = !User::isEveryoneAllowed( 'read' ) && @@ -98,6 +98,57 @@ class RawAction extends FormlessAction { 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage ); + // In the event of user JS, don't allow loading a user JS/CSS/Json + // subpage that has no registered user associated with, as + // someone could register the account and take control of the + // JS/CSS/Json page. + $title = $this->getTitle(); + if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) { + // not using getRootText() as we want this to work + // even if subpages are disabled. + $rootPage = strtok( $title->getText(), '/' ); + $userFromTitle = User::newFromName( $rootPage, 'usable' ); + if ( !$userFromTitle || $userFromTitle->getId() === 0 ) { + $log = LoggerFactory::getInstance( "security" ); + $log->warning( + "Unsafe JS/CSS/Json load - {user} loaded {title} with {ctype}", + [ + 'user' => $this->getUser()->getName(), + 'title' => $title->getPrefixedDBKey(), + 'ctype' => $contentType, + ] + ); + $msg = wfMessage( 'unregistered-user-config' ); + throw new HttpError( 403, $msg ); + } + } + + // Don't allow loading non-protected pages as javascript. + // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT + // in NS_MEDIAWIKI or NS_USER, as well as including other config types, + // but for now be more permissive. Allowing protected pages outside of + // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary + // allowance. + if ( + $contentType === 'text/javascript' && + !$title->isUserJsConfigPage() && + !$title->inNamespace( NS_MEDIAWIKI ) && + !in_array( 'sysop', $title->getRestrictions( 'edit' ) ) && + !in_array( 'editprotected', $title->getRestrictions( 'edit' ) ) + ) { + + $log = LoggerFactory::getInstance( "security" ); + $log->info( "Blocked loading unprotected JS {title} for {user}", + [ + 'user' => $this->getUser()->getName(), + 'title' => $title->getPrefixedDBKey(), + ] + ); + throw new HttpError( 403, wfMessage( 'unprotected-js' ) ); + } + + $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' ); + $text = $this->getRawText(); // Don't return a 404 response for CSS or JavaScript; @@ -166,7 +217,7 @@ class RawAction extends FormlessAction { } if ( $content === null || $content === false ) { - // section not found (or section not supported, e.g. for JS and CSS) + // section not found (or section not supported, e.g. for JS, JSON, and CSS) $text = false; } else { $text = $content->getNativeData(); @@ -175,7 +226,7 @@ class RawAction extends FormlessAction { } } - if ( $text !== false && $text !== '' && $request->getVal( 'templates' ) === 'expand' ) { + if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) { $text = $wgParser->preprocess( $text, $title, @@ -225,10 +276,14 @@ class RawAction extends FormlessAction { * @return string */ public function getContentType() { - $ctype = $this->getRequest()->getVal( 'ctype' ); + // Use getRawVal instead of getVal because we only + // need to match against known strings, there is no + // storing of localised content or other user input. + $ctype = $this->getRequest()->getRawVal( 'ctype' ); if ( $ctype == '' ) { - $gen = $this->getRequest()->getVal( 'gen' ); + // Legacy compatibilty + $gen = $this->getRequest()->getRawVal( 'gen' ); if ( $gen == 'js' ) { $ctype = 'text/javascript'; } elseif ( $gen == 'css' ) { @@ -236,7 +291,14 @@ class RawAction extends FormlessAction { } } - $allowedCTypes = [ 'text/x-wiki', 'text/javascript', 'text/css', 'application/x-zope-edit' ]; + $allowedCTypes = [ + 'text/x-wiki', + 'text/javascript', + 'text/css', + // FIXME: Should we still allow Zope editing? External editing feature was dropped + 'application/x-zope-edit', + 'application/json' + ]; if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) { $ctype = 'text/x-wiki'; }