Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

resourceloader/ResourceLoaderStartUpModule.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Module for ResourceLoader initialization.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Trevor Parscal
22
 * @author Roan Kattouw
23
 */
24
25
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
26
27
	// Cache for getConfigSettings() as it's called by multiple methods
28
	protected $configVars = [];
29
	protected $targets = [ 'desktop', 'mobile' ];
30
31
	/**
32
	 * @param ResourceLoaderContext $context
33
	 * @return array
34
	 */
35
	protected function getConfigSettings( $context ) {
36
37
		$hash = $context->getHash();
38
		if ( isset( $this->configVars[$hash] ) ) {
39
			return $this->configVars[$hash];
40
		}
41
42
		global $wgContLang;
43
		$conf = $this->getConfig();
44
45
		// We can't use Title::newMainPage() if 'mainpage' is in
46
		// $wgForceUIMsgAsContentMsg because that will try to use the session
47
		// user's language and we have no session user. This does the
48
		// equivalent but falling back to our ResourceLoaderContext language
49
		// instead.
50
		$mainPage = Title::newFromText( $context->msg( 'mainpage' )->inContentLanguage()->text() );
51
		if ( !$mainPage ) {
52
			$mainPage = Title::newFromText( 'Main Page' );
53
		}
54
55
		/**
56
		 * Namespace related preparation
57
		 * - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
58
		 * - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
59
		 */
60
		$namespaceIds = $wgContLang->getNamespaceIds();
61
		$caseSensitiveNamespaces = [];
62
		foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
0 ignored issues
show
The expression \MWNamespace::getCanonicalNamespaces() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
63
			$namespaceIds[$wgContLang->lc( $name )] = $index;
64
			if ( !MWNamespace::isCapitalized( $index ) ) {
65
				$caseSensitiveNamespaces[] = $index;
66
			}
67
		}
68
69
		$illegalFileChars = $conf->get( 'IllegalFileChars' );
70
71
		// Build list of variables
72
		$vars = [
73
			'wgLoadScript' => wfScript( 'load' ),
74
			'debug' => $context->getDebug(),
75
			'skin' => $context->getSkin(),
76
			'stylepath' => $conf->get( 'StylePath' ),
77
			'wgUrlProtocols' => wfUrlProtocols(),
78
			'wgArticlePath' => $conf->get( 'ArticlePath' ),
79
			'wgScriptPath' => $conf->get( 'ScriptPath' ),
80
			'wgScriptExtension' => '.php',
81
			'wgScript' => wfScript(),
82
			'wgSearchType' => $conf->get( 'SearchType' ),
83
			'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
84
			// Force object to avoid "empty" associative array from
85
			// becoming [] instead of {} in JS (bug 34604)
86
			'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
87
			'wgServer' => $conf->get( 'Server' ),
88
			'wgServerName' => $conf->get( 'ServerName' ),
89
			'wgUserLanguage' => $context->getLanguage(),
90
			'wgContentLanguage' => $wgContLang->getCode(),
91
			'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
92
			'wgVersion' => $conf->get( 'Version' ),
93
			'wgEnableAPI' => $conf->get( 'EnableAPI' ),
94
			'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ),
95
			'wgMainPageTitle' => $mainPage->getPrefixedText(),
96
			'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
97
			'wgNamespaceIds' => $namespaceIds,
98
			'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
99
			'wgSiteName' => $conf->get( 'Sitename' ),
100
			'wgDBname' => $conf->get( 'DBname' ),
101
			'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
102
			'wgAvailableSkins' => Skin::getSkinNames(),
103
			'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
104
			// MediaWiki sets cookies to have this prefix by default
105
			'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
106
			'wgCookieDomain' => $conf->get( 'CookieDomain' ),
107
			'wgCookiePath' => $conf->get( 'CookiePath' ),
108
			'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
109
			'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
110
			'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
111
			'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
112
			'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
113
			'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
114
			'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
115
			'wgResourceLoaderLegacyModules' => self::getLegacyModules(),
116
			'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
117
			'wgEnableUploads' => $conf->get( 'EnableUploads' ),
118
		];
119
120
		Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
121
122
		$this->configVars[$hash] = $vars;
123
		return $this->configVars[$hash];
124
	}
125
126
	/**
127
	 * Recursively get all explicit and implicit dependencies for to the given module.
128
	 *
129
	 * @param array $registryData
130
	 * @param string $moduleName
131
	 * @return array
132
	 */
133
	protected static function getImplicitDependencies( array $registryData, $moduleName ) {
134
		static $dependencyCache = [];
135
136
		// The list of implicit dependencies won't be altered, so we can
137
		// cache them without having to worry.
138
		if ( !isset( $dependencyCache[$moduleName] ) ) {
139
140
			if ( !isset( $registryData[$moduleName] ) ) {
141
				// Dependencies may not exist
142
				$dependencyCache[$moduleName] = [];
143
			} else {
144
				$data = $registryData[$moduleName];
145
				$dependencyCache[$moduleName] = $data['dependencies'];
146
147
				foreach ( $data['dependencies'] as $dependency ) {
148
					// Recursively get the dependencies of the dependencies
149
					$dependencyCache[$moduleName] = array_merge(
150
						$dependencyCache[$moduleName],
151
						self::getImplicitDependencies( $registryData, $dependency )
152
					);
153
				}
154
			}
155
		}
156
157
		return $dependencyCache[$moduleName];
158
	}
159
160
	/**
161
	 * Optimize the dependency tree in $this->modules.
162
	 *
163
	 * The optimization basically works like this:
164
	 *	Given we have module A with the dependencies B and C
165
	 *		and module B with the dependency C.
166
	 *	Now we don't have to tell the client to explicitly fetch module
167
	 *		C as that's already included in module B.
168
	 *
169
	 * This way we can reasonably reduce the amount of module registration
170
	 * data send to the client.
171
	 *
172
	 * @param array &$registryData Modules keyed by name with properties:
173
	 *  - string 'version'
174
	 *  - array 'dependencies'
175
	 *  - string|null 'group'
176
	 *  - string 'source'
177
	 */
178
	public static function compileUnresolvedDependencies( array &$registryData ) {
179
		foreach ( $registryData as $name => &$data ) {
180
			$dependencies = $data['dependencies'];
181
			foreach ( $data['dependencies'] as $dependency ) {
182
				$implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
183
				$dependencies = array_diff( $dependencies, $implicitDependencies );
184
			}
185
			// Rebuild keys
186
			$data['dependencies'] = array_values( $dependencies );
187
		}
188
	}
189
190
	/**
191
	 * Get registration code for all modules.
192
	 *
193
	 * @param ResourceLoaderContext $context
194
	 * @return string JavaScript code for registering all modules with the client loader
195
	 */
196
	public function getModuleRegistrations( ResourceLoaderContext $context ) {
197
198
		$resourceLoader = $context->getResourceLoader();
199
		$target = $context->getRequest()->getVal( 'target', 'desktop' );
200
		// Bypass target filter if this request is Special:JavaScriptTest.
201
		// To prevent misuse in production, this is only allowed if testing is enabled server-side.
202
		$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
203
204
		$out = '';
205
		$registryData = [];
206
207
		// Get registry data
208
		foreach ( $resourceLoader->getModuleNames() as $name ) {
209
			$module = $resourceLoader->getModule( $name );
210
			$moduleTargets = $module->getTargets();
211
			if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) {
212
				continue;
213
			}
214
215
			if ( $module->isRaw() ) {
216
				// Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because
217
				// depending on them is illegal anyway and would only lead to them being reloaded
218
				// causing any state to be lost (like jQuery plugins, mw.config etc.)
219
				continue;
220
			}
221
222
			$versionHash = $module->getVersionHash( $context );
223
			if ( strlen( $versionHash ) !== 7 ) {
224
				$context->getLogger()->warning(
225
					"Module '{module}' produced an invalid version hash: '{version}'.",
226
					[
227
						'module' => $name,
228
						'version' => $versionHash,
229
					]
230
				);
231
				// Module implementation either broken or deviated from ResourceLoader::makeHash
232
				// Asserted by tests/phpunit/structure/ResourcesTest.
233
				$versionHash = ResourceLoader::makeHash( $versionHash );
234
			}
235
236
			$skipFunction = $module->getSkipFunction();
237
			if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
238
				$skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
239
			}
240
241
			$registryData[$name] = [
242
				'version' => $versionHash,
243
				'dependencies' => $module->getDependencies( $context ),
244
				'group' => $module->getGroup(),
245
				'source' => $module->getSource(),
246
				'skip' => $skipFunction,
247
			];
248
		}
249
250
		self::compileUnresolvedDependencies( $registryData );
251
252
		// Register sources
253
		$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
254
255
		// Figure out the different call signatures for mw.loader.register
256
		$registrations = [];
257
		foreach ( $registryData as $name => $data ) {
258
			// Call mw.loader.register(name, version, dependencies, group, source, skip)
259
			$registrations[] = [
260
				$name,
261
				$data['version'],
262
				$data['dependencies'],
263
				$data['group'],
264
				// Swap default (local) for null
265
				$data['source'] === 'local' ? null : $data['source'],
266
				$data['skip']
267
			];
268
		}
269
270
		// Register modules
271
		$out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
272
273
		return $out;
274
	}
275
276
	/**
277
	 * @return bool
278
	 */
279
	public function isRaw() {
280
		return true;
281
	}
282
283
	/**
284
	 * Base modules required for the base environment of ResourceLoader
285
	 *
286
	 * @return array
287
	 */
288
	public static function getStartupModules() {
289
		return [ 'jquery', 'mediawiki' ];
290
	}
291
292
	public static function getLegacyModules() {
293
		global $wgIncludeLegacyJavaScript;
294
295
		$legacyModules = [];
296
		if ( $wgIncludeLegacyJavaScript ) {
297
			$legacyModules[] = 'mediawiki.legacy.wikibits';
298
		}
299
300
		return $legacyModules;
301
	}
302
303
	/**
304
	 * Get the load URL of the startup modules.
305
	 *
306
	 * This is a helper for getScript(), but can also be called standalone, such
307
	 * as when generating an AppCache manifest.
308
	 *
309
	 * @param ResourceLoaderContext $context
310
	 * @return string
311
	 */
312
	public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
313
		$rl = $context->getResourceLoader();
314
315
		$derivative = new DerivativeResourceLoaderContext( $context );
316
		$derivative->setModules( self::getStartupModules() );
317
		$derivative->setOnly( 'scripts' );
318
		// Must setModules() before makeVersionQuery()
319
		$derivative->setVersion( $rl->makeVersionQuery( $derivative ) );
0 ignored issues
show
It seems like $rl->makeVersionQuery($derivative) targeting ResourceLoader::makeVersionQuery() can also be of type false; however, DerivativeResourceLoaderContext::setVersion() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
320
321
		return $rl->createLoaderURL( 'local', $derivative );
322
	}
323
324
	/**
325
	 * @param ResourceLoaderContext $context
326
	 * @return string
327
	 */
328
	public function getScript( ResourceLoaderContext $context ) {
329
		global $IP;
330
		if ( $context->getOnly() !== 'scripts' ) {
331
			return '/* Requires only=script */';
332
		}
333
334
		$out = file_get_contents( "$IP/resources/src/startup.js" );
335
336
		$pairs = array_map( function ( $value ) {
337
			$value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK );
338
			// Fix indentation
339
			$value = str_replace( "\n", "\n\t", $value );
340
			return $value;
341
		}, [
342
			'$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
343
			'$VARS.configuration' => $this->getConfigSettings( $context ),
344
			'$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
345
		] );
346
		$pairs['$CODE.registrations()'] = str_replace(
347
			"\n",
348
			"\n\t",
349
			trim( $this->getModuleRegistrations( $context ) )
350
		);
351
352
		return strtr( $out, $pairs );
353
	}
354
355
	/**
356
	 * @return bool
357
	 */
358
	public function supportsURLLoading() {
359
		return false;
360
	}
361
362
	/**
363
	 * Get the definition summary for this module.
364
	 *
365
	 * @param ResourceLoaderContext $context
366
	 * @return array
367
	 */
368
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
369
		global $IP;
370
		$summary = parent::getDefinitionSummary( $context );
371
		$summary[] = [
372
			// Detect changes to variables exposed in mw.config (T30899).
373
			'vars' => $this->getConfigSettings( $context ),
374
			// Changes how getScript() creates mw.Map for mw.config
375
			'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
376
			// Detect changes to the module registrations
377
			'moduleHashes' => $this->getAllModuleHashes( $context ),
378
379
			'fileMtimes' => [
380
				filemtime( "$IP/resources/src/startup.js" ),
381
			],
382
		];
383
		return $summary;
384
	}
385
386
	/**
387
	 * Helper method for getDefinitionSummary().
388
	 *
389
	 * @param ResourceLoaderContext $context
390
	 * @return string SHA-1
391
	 */
392
	protected function getAllModuleHashes( ResourceLoaderContext $context ) {
393
		$rl = $context->getResourceLoader();
394
		// Preload for getCombinedVersion()
395
		$rl->preloadModuleInfo( $rl->getModuleNames(), $context );
396
397
		// ATTENTION: Because of the line below, this is not going to cause infinite recursion.
398
		// Think carefully before making changes to this code!
399
		// Pre-populate versionHash with something because the loop over all modules below includes
400
		// the startup module (this module).
401
		// See ResourceLoaderModule::getVersionHash() for usage of this cache.
402
		$this->versionHash[$context->getHash()] = null;
403
404
		return $rl->getCombinedVersion( $context, $rl->getModuleNames() );
405
	}
406
407
	/**
408
	 * @return string
409
	 */
410
	public function getGroup() {
411
		return 'startup';
412
	}
413
}
414