Follow-up r64670 (bug22929): cleaner implementation of security for script (and potentially CSS) files. ResourceLoader *already* knows where each module has come from, so all we need to do is filter them in OutputPage according to the desired level of 'trustworthiness'.

TODO:
* Are there instances where we might want to restrict CSS as well as JS?
* Would a $wg config option and/or user preference and/or index.php GET parameter to limit inclusion be useful?
* Can we deprecate any of the existing $wg config options?
* What's going on with the duplicated code between OutputPage and SkinTemplate?
This commit is contained in:
Happy-melon 2011-02-04 16:39:17 +00:00
parent 47529179e9
commit da36f65433
6 changed files with 163 additions and 40 deletions

View file

@ -124,9 +124,12 @@ class OutputPage {
var $mTemplateIds = array();
/** Initialized with a global value. Let us override it.
* Should probably get deleted / rewritten ... */
var $mAllowUserJs;
# What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
# @see ResourceLoaderModule::$origin
# ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
protected $mAllowedModules = array(
ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
);
/**
* @EasterEgg I just love the name for this self documenting variable.
@ -211,15 +214,6 @@ class OutputPage {
'Cookie' => null
);
/**
* Constructor
* Initialise private variables
*/
function __construct() {
global $wgAllowUserJs;
$this->mAllowUserJs = $wgAllowUserJs;
}
/**
* Redirect to $url rather than displaying the normal page
*
@ -366,13 +360,34 @@ class OutputPage {
return $this->mScripts . $this->getHeadItems();
}
/**
* Filter an array of modules to remove insufficiently trustworthy members
* @param $modules Array
* @return Array
*/
protected function filterModules( $modules, $type = ResourceLoaderModule::TYPE_COMBINED ){
$resourceLoader = $this->getResourceLoader();
$filteredModules = array();
foreach( $modules as $val ){
$module = $resourceLoader->getModule( $val );
if( $module->getOrigin() <= $this->getAllowedModules( $type ) ) {
$filteredModules[] = $val;
}
}
return $filteredModules;
}
/**
* Get the list of modules to include on this page
*
* @param $filter Bool whether to filter out insufficiently trustworthy modules
* @return Array of module names
*/
public function getModules() {
return array_values( array_unique( $this->mModules ) );
public function getModules( $filter = false, $param = 'mModules' ) {
$modules = array_values( array_unique( $this->$param ) );
return $filter
? $this->filterModules( $modules )
: $modules;
}
/**
@ -390,8 +405,8 @@ class OutputPage {
* Get the list of module JS to include on this page
* @return array of module names
*/
public function getModuleScripts() {
return array_values( array_unique( $this->mModuleScripts ) );
public function getModuleScripts( $filter = false ) {
return $this->getModules( $filter, 'mModuleScripts' );
}
/**
@ -410,8 +425,8 @@ class OutputPage {
*
* @return Array of module names
*/
public function getModuleStyles() {
return array_values( array_unique( $this->mModuleStyles ) );
public function getModuleStyles( $filter = false ) {
return $this->getModules( $filter, 'mModuleStyles' );
}
/**
@ -430,8 +445,8 @@ class OutputPage {
*
* @return Array of module names
*/
public function getModuleMessages() {
return array_values( array_unique( $this->mModuleMessages ) );
public function getModuleMessages( $filter = false ) {
return $this->getModules( $filter, 'mModuleMessages' );
}
/**
@ -1065,19 +1080,58 @@ class OutputPage {
}
/**
* Remove user JavaScript from scripts to load
* Do not allow scripts which can be modified by wiki users to load on this page;
* only allow scripts bundled with, or generated by, the software.
*/
public function disallowUserJs() {
$this->mAllowUserJs = false;
$this->reduceAllowedModules(
ResourceLoaderModule::TYPE_SCRIPTS,
ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
);
}
/**
* Return whether user JavaScript is allowed for this page
*
* @deprecated @since 1.18 Load modules with ResourceLoader, and origin and
* trustworthiness is identified and enforced automagically.
* @return Boolean
*/
public function isUserJsAllowed() {
return $this->mAllowUserJs;
return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >= ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL;
}
/**
* Show what level of JavaScript / CSS untrustworthiness is allowed on this page
* @see ResourceLoaderModule::$origin
* @param $type String ResourceLoaderModule TYPE_ constant
* @return Int ResourceLoaderModule ORIGIN_ class constant
*/
public function getAllowedModules( $type ){
if( $type == ResourceLoaderModule::TYPE_COMBINED ){
return min( array_values( $this->mAllowedModules ) );
} else {
return isset( $this->mAllowedModules[$type] )
? $this->mAllowedModules[$type]
: ResourceLoaderModule::ORIGIN_ALL;
}
}
/**
* Set the highest level of CSS/JS untrustworthiness allowed
* @param $type String ResourceLoaderModule TYPE_ constant
* @param $level Int ResourceLoaderModule class constant
*/
public function setAllowedModules( $type, $level ){
$this->mAllowedModules[$type] = $level;
}
/**
* As for setAllowedModules(), but don't inadvertantly make the page more accessible
* @param $type String
* @param $level Int ResourceLoaderModule class constant
*/
public function reduceAllowedModules( $type, $level ){
$this->mAllowedModules[$type] = min( $this->getAllowedModules($type), $level );
}
/**
@ -2347,7 +2401,7 @@ class OutputPage {
* TODO: Document
* @param $skin Skin
* @param $modules Array/string with the module name
* @param $only string May be styles, messages or scripts
* @param $only String ResourceLoaderModule TYPE_ class constant
* @param $useESI boolean
* @return string html <script> and <style> tags
*/
@ -2396,12 +2450,23 @@ class OutputPage {
$resourceLoader = $this->getResourceLoader();
foreach ( (array) $modules as $name ) {
$module = $resourceLoader->getModule( $name );
# Check that we're allowed to include this module on this page
if( ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
&& $only == ResourceLoaderModule::TYPE_SCRIPTS )
|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
&& $only == ResourceLoaderModule::TYPE_STYLES )
)
{
continue;
}
$group = $module->getGroup();
if ( !isset( $groups[$group] ) ) {
$groups[$group] = array();
}
$groups[$group][$name] = $module;
}
$links = '';
foreach ( $groups as $group => $modules ) {
$query['modules'] = implode( '|', array_keys( $modules ) );
@ -2412,7 +2477,7 @@ class OutputPage {
// Support inlining of private modules if configured as such
if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) {
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
if ( $only == 'styles' ) {
if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
$links .= Html::inlineStyle(
$resourceLoader->makeModuleResponse( $context, $modules )
);
@ -2446,14 +2511,14 @@ class OutputPage {
$url = wfAppendQuery( $wgLoadScript, $query );
if ( $useESI && $wgResourceLoaderUseESI ) {
$esi = Xml::element( 'esi:include', array( 'src' => $url ) );
if ( $only == 'styles' ) {
if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
$links .= Html::inlineStyle( $esi );
} else {
$links .= Html::inlineScript( $esi );
}
} else {
// Automatically select style/script elements
if ( $only === 'styles' ) {
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
$links .= Html::linkedStyle( wfAppendQuery( $wgLoadScript, $query ) ) . "\n";
} else {
$links .= Html::linkedScript( wfAppendQuery( $wgLoadScript, $query ) ) . "\n";
@ -2472,24 +2537,24 @@ class OutputPage {
* @return String: HTML fragment
*/
function getHeadScripts( Skin $sk ) {
global $wgUser, $wgRequest, $wgUseSiteJs;
global $wgUser, $wgRequest, $wgUseSiteJs, $wgAllowUserJs;
// Startup - this will immediately load jquery and mediawiki modules
$scripts = $this->makeResourceLoaderLink( $sk, 'startup', 'scripts', true );
$scripts = $this->makeResourceLoaderLink( $sk, 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
// Configuration -- This could be merged together with the load and go, but
// makeGlobalVariablesScript returns a whole script tag -- grumble grumble...
$scripts .= Skin::makeGlobalVariablesScript( $sk->getSkinName() ) . "\n";
// Script and Messages "only" requests
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts(), 'scripts' );
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages(), 'messages' );
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts( true ), ResourceLoaderModule::TYPE_SCRIPTS );
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages( true ), ResourceLoaderModule::TYPE_MESSAGES );
// Modules requests - let the client calculate dependencies and batch requests as it likes
if ( $this->getModules() ) {
if ( $this->getModules( true ) ) {
$scripts .= Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
Xml::encodeJsCall( 'mediaWiki.loader.load', array( $this->getModules() ) ) .
Xml::encodeJsCall( 'mediaWiki.loader.load', array( $this->getModules( true ) ) ) .
Xml::encodeJsCall( 'mediaWiki.loader.go', array() )
)
) . "\n";
@ -2500,25 +2565,25 @@ class OutputPage {
// Add site JS if enabled
if ( $wgUseSiteJs ) {
$scripts .= $this->makeResourceLoaderLink( $sk, 'site', 'scripts' );
$scripts .= $this->makeResourceLoaderLink( $sk, 'site', ResourceLoaderModule::TYPE_SCRIPTS );
}
// Add user JS if enabled - trying to load user.options as a bundle if possible
$userOptionsAdded = false;
if ( $this->isUserJsAllowed() && $wgUser->isLoggedIn() ) {
if ( $wgAllowUserJs && $wgUser->isLoggedIn() ) {
$action = $wgRequest->getVal( 'action', 'view' );
if( $this->mTitle && $this->mTitle->isJsSubpage() && $sk->userCanPreview( $action ) ) {
# XXX: additional security check/prompt?
$scripts .= Html::inlineScript( "\n" . $wgRequest->getText( 'wpTextbox1' ) . "\n" ) . "\n";
} else {
$scripts .= $this->makeResourceLoaderLink(
$sk, array( 'user', 'user.options' ), 'scripts'
$sk, array( 'user', 'user.options' ), ResourceLoaderModule::TYPE_SCRIPTS
);
$userOptionsAdded = true;
}
}
if ( !$userOptionsAdded ) {
$scripts .= $this->makeResourceLoaderLink( $sk, 'user.options', 'scripts' );
$scripts .= $this->makeResourceLoaderLink( $sk, 'user.options', ResourceLoaderModule::TYPE_SCRIPTS );
}
return $scripts;
@ -2713,7 +2778,7 @@ class OutputPage {
// dynamically added styles to override statically added styles from other modules. So the order
// has to be other, dynamic, site, user
// Add statically added styles for other modules
$ret .= $this->makeResourceLoaderLink( $sk, $styles['other'], 'styles' );
$ret .= $this->makeResourceLoaderLink( $sk, $styles['other'], ResourceLoaderModule::TYPE_STYLES );
// Add normal styles added through addStyle()/addInlineStyle() here
$ret .= implode( "\n", $this->buildCssLinksArray() ) . $this->mInlineStyles;
// Add marker tag to mark the place where the client-side loader should inject dynamic styles
@ -2721,7 +2786,7 @@ class OutputPage {
$ret .= Html::element( 'meta', array( 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ) );
// Add site and user styles
$ret .= $this->makeResourceLoaderLink(
$sk, array_merge( $styles['site'], $styles['user'] ), 'styles'
$sk, array_merge( $styles['site'], $styles['user'] ), ResourceLoaderModule::TYPE_STYLES
);
return $ret;
}

View file

@ -201,6 +201,8 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'usercss', $this->usercss );
$this->userjs = $this->userjsprev = false;
# FIXME: this is the only use of OutputPage::isUserJsAllowed() anywhere; can we
# get rid of it? For that matter, why is any of this here at all?
$this->setupUserJs( $out->isUserJsAllowed() );
$tpl->setRef( 'userjs', $this->userjs );
$tpl->setRef( 'userjsprev', $this->userjsprev );
@ -1255,6 +1257,7 @@ class SkinTemplate extends Skin {
/**
* @private
* FIXME: why is this duplicated in/from OutputPage::getHeadScripts()??
*/
function setupUserJs( $allowUserJs ) {
global $wgRequest, $wgJsMimeType;

View file

@ -24,6 +24,34 @@
* Abstraction for resource loader modules, with name registration and maxage functionality.
*/
abstract class ResourceLoaderModule {
# Type of resource
const TYPE_SCRIPTS = 'scripts';
const TYPE_STYLES = 'styles';
const TYPE_MESSAGES = 'messages';
const TYPE_COMBINED = 'combined';
# sitewide core module like a skin file or jQuery component
const ORIGIN_CORE_SITEWIDE = 1;
# per-user module generated by the software
const ORIGIN_CORE_INDIVIDUAL = 2;
# sitewide module generated from user-editable files, like MediaWiki:Common.js, or
# modules accessible to multiple users, such as those generated by the Gadgets extension.
const ORIGIN_USER_SITEWIDE = 3;
# per-user module generated from user-editable files, like User:Me/Monobook.js
const ORIGIN_USER_INDIVIDUAL = 4;
# an access constant; make sure this is kept as the largest number in this group
const ORIGIN_ALL = 10;
# script and style modules form a hierarchy of trustworthiness, with core modules like
# skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
# limit the types of scripts and styles we allow to load on, say, sensitive special
# pages like Special:UserLogin and Special:Preferences
protected $origin = self::ORIGIN_CORE_SITEWIDE;
/* Protected Members */
@ -56,6 +84,27 @@ abstract class ResourceLoaderModule {
$this->name = $name;
}
/**
* Get this module's origin. This is set when the module is registered
* with ResourceLoader::register()
*
* @return Int ResourceLoaderModule class constant, the subclass default
* if not set manuall
*/
public function getOrigin() {
return $this->origin;
}
/**
* Set this module's origin. This is called by ResourceLodaer::register()
* when registering the module. Other code should not call this.
*
* @param $name Int origin
*/
public function setOrigin( $origin ) {
$this->origin = $origin;
}
/**
* Get whether CSS for this module should be flipped
* @param $context ResourceLoaderContext

View file

@ -26,6 +26,7 @@
class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
/* Protected Methods */
protected $origin = self::ORIGIN_USER_INDIVIDUAL;
protected function getPages( ResourceLoaderContext $context ) {
if ( $context->getUser() ) {

View file

@ -29,6 +29,8 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
protected $modifiedTime = array();
protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
/* Methods */
public function getModifiedTime( ResourceLoaderContext $context ) {

View file

@ -32,6 +32,9 @@ defined( 'MEDIAWIKI' ) || die( 1 );
abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
/* Protected Members */
# Origin is user-supplied code
protected $origin = self::ORIGIN_USER_SITEWIDE;
// In-object cache for modified time
protected $modifiedTime = array();