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.

includes/specials/SpecialUploadStash.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
 * Implements Special:UploadStash.
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
 * @ingroup SpecialPage
22
 * @ingroup Upload
23
 */
24
25
/**
26
 * Web access for files temporarily stored by UploadStash.
27
 *
28
 * For example -- files that were uploaded with the UploadWizard extension are stored temporarily
29
 * before committing them to the db. But we want to see their thumbnails and get other information
30
 * about them.
31
 *
32
 * Since this is based on the user's session, in effect this creates a private temporary file area.
33
 * However, the URLs for the files cannot be shared.
34
 */
35
class SpecialUploadStash extends UnlistedSpecialPage {
36
	// UploadStash
37
	private $stash;
38
39
	/**
40
	 * Since we are directly writing the file to STDOUT,
41
	 * we should not be reading in really big files and serving them out.
42
	 *
43
	 * We also don't want people using this as a file drop, even if they
44
	 * share credentials.
45
	 *
46
	 * This service is really for thumbnails and other such previews while
47
	 * uploading.
48
	 */
49
	const MAX_SERVE_BYTES = 1048576; // 1MB
50
51
	public function __construct() {
52
		parent::__construct( 'UploadStash', 'upload' );
53
	}
54
55
	public function doesWrites() {
56
		return true;
57
	}
58
59
	/**
60
	 * Execute page -- can output a file directly or show a listing of them.
61
	 *
62
	 * @param string $subPage Subpage, e.g. in
63
	 *   https://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
64
	 * @return bool Success
65
	 */
66
	public function execute( $subPage ) {
67
		$this->useTransactionalTimeLimit();
68
69
		$this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
70
		$this->checkPermissions();
71
72
		if ( $subPage === null || $subPage === '' ) {
73
			return $this->showUploads();
74
		}
75
76
		return $this->showUpload( $subPage );
77
	}
78
79
	/**
80
	 * If file available in stash, cats it out to the client as a simple HTTP response.
81
	 * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward.
82
	 *
83
	 * @param string $key The key of a particular requested file
84
	 * @throws HttpError
85
	 * @return bool
86
	 */
87
	public function showUpload( $key ) {
88
		// prevent callers from doing standard HTML output -- we'll take it from here
89
		$this->getOutput()->disable();
90
91
		try {
92
			$params = $this->parseKey( $key );
93
			if ( $params['type'] === 'thumb' ) {
94
				return $this->outputThumbFromStash( $params['file'], $params['params'] );
95
			} else {
96
				return $this->outputLocalFile( $params['file'] );
97
			}
98
		} catch ( UploadStashFileNotFoundException $e ) {
99
			$code = 404;
100
			$message = $e->getMessage();
101
		} catch ( UploadStashZeroLengthFileException $e ) {
102
			$code = 500;
103
			$message = $e->getMessage();
104
		} catch ( UploadStashBadPathException $e ) {
105
			$code = 500;
106
			$message = $e->getMessage();
107
		} catch ( SpecialUploadStashTooLargeException $e ) {
108
			$code = 500;
109
			$message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES .
110
				' bytes. ' . $e->getMessage();
111
		} catch ( Exception $e ) {
112
			$code = 500;
113
			$message = $e->getMessage();
114
		}
115
116
		throw new HttpError( $code, $message );
117
	}
118
119
	/**
120
	 * Parse the key passed to the SpecialPage. Returns an array containing
121
	 * the associated file object, the type ('file' or 'thumb') and if
122
	 * application the transform parameters
123
	 *
124
	 * @param string $key
125
	 * @throws UploadStashBadPathException
126
	 * @return array
127
	 */
128
	private function parseKey( $key ) {
129
		$type = strtok( $key, '/' );
130
131
		if ( $type !== 'file' && $type !== 'thumb' ) {
132
			throw new UploadStashBadPathException( "Unknown type '$type'" );
133
		}
134
		$fileName = strtok( '/' );
135
		$thumbPart = strtok( '/' );
136
		$file = $this->stash->getFile( $fileName );
137
		if ( $type === 'thumb' ) {
138
			$srcNamePos = strrpos( $thumbPart, $fileName );
139
			if ( $srcNamePos === false || $srcNamePos < 1 ) {
140
				throw new UploadStashBadPathException( 'Unrecognized thumb name' );
141
			}
142
			$paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
143
144
			$handler = $file->getHandler();
145
			if ( $handler ) {
146
				$params = $handler->parseParamString( $paramString );
147
148
				return [ 'file' => $file, 'type' => $type, 'params' => $params ];
149
			} else {
150
				throw new UploadStashBadPathException( 'No handler found for ' .
151
					"mime {$file->getMimeType()} of file {$file->getPath()}" );
152
			}
153
		}
154
155
		return [ 'file' => $file, 'type' => $type ];
156
	}
157
158
	/**
159
	 * Get a thumbnail for file, either generated locally or remotely, and stream it out
160
	 *
161
	 * @param File $file
162
	 * @param array $params
163
	 *
164
	 * @return bool Success
165
	 */
166
	private function outputThumbFromStash( $file, $params ) {
167
		$flags = 0;
168
		// this config option, if it exists, points to a "scaler", as you might find in
169
		// the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This
170
		// is part of our horrible NFS-based system, we create a file on a mount
171
		// point here, but fetch the scaled file from somewhere else that
172
		// happens to share it over NFS.
173
		if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) {
174
			$this->outputRemoteScaledThumb( $file, $params, $flags );
175
		} else {
176
			$this->outputLocallyScaledThumb( $file, $params, $flags );
177
		}
178
	}
179
180
	/**
181
	 * Scale a file (probably with a locally installed imagemagick, or similar)
182
	 * and output it to STDOUT.
183
	 * @param File $file
184
	 * @param array $params Scaling parameters ( e.g. [ width => '50' ] );
185
	 * @param int $flags Scaling flags ( see File:: constants )
186
	 * @throws MWException|UploadStashFileNotFoundException
187
	 * @return bool Success
188
	 */
189
	private function outputLocallyScaledThumb( $file, $params, $flags ) {
190
		// n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely
191
		// on HTTP caching to ensure this doesn't happen.
192
193
		$flags |= File::RENDER_NOW;
194
195
		$thumbnailImage = $file->transform( $params, $flags );
196
		if ( !$thumbnailImage ) {
197
			throw new MWException( 'Could not obtain thumbnail' );
198
		}
199
200
		// we should have just generated it locally
201
		if ( !$thumbnailImage->getStoragePath() ) {
202
			throw new UploadStashFileNotFoundException( "no local path for scaled item" );
203
		}
204
205
		// now we should construct a File, so we can get MIME and other such info in a standard way
206
		// n.b. MIME type may be different from original (ogx original -> jpeg thumb)
207
		$thumbFile = new UnregisteredLocalFile( false,
208
			$this->stash->repo, $thumbnailImage->getStoragePath(), false );
209
		if ( !$thumbFile ) {
210
			throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
211
		}
212
213
		return $this->outputLocalFile( $thumbFile );
214
	}
215
216
	/**
217
	 * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation
218
	 * cluster, and output it to STDOUT.
219
	 * Note: Unlike the usual thumbnail process, the web client never sees the
220
	 * cluster URL; we do the whole HTTP transaction to the scaler ourselves
221
	 * and cat the results out.
222
	 * Note: We rely on NFS to have propagated the file contents to the scaler.
223
	 * However, we do not rely on the thumbnail being created in NFS and then
224
	 * propagated back to our filesystem. Instead we take the results of the
225
	 * HTTP request instead.
226
	 * Note: No caching is being done here, although we are instructing the
227
	 * client to cache it forever.
228
	 *
229
	 * @param File $file
230
	 * @param array $params Scaling parameters ( e.g. [ width => '50' ] );
231
	 * @param int $flags Scaling flags ( see File:: constants )
232
	 * @throws MWException
233
	 * @return bool Success
234
	 */
235
	private function outputRemoteScaledThumb( $file, $params, $flags ) {
236
		// This option probably looks something like
237
		// '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use
238
		// trailing slash.
239
		$scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' );
240
241
		if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) {
242
			// this is apparently a protocol-relative URL, which makes no sense in this context,
243
			// since this is used for communication that's internal to the application.
244
			// default to http.
245
			$scalerBaseUrl = wfExpandUrl( $scalerBaseUrl, PROTO_CANONICAL );
246
		}
247
248
		// We need to use generateThumbName() instead of thumbName(), because
249
		// the suffix needs to match the file name for the remote thumbnailer
250
		// to work
251
		$scalerThumbName = $file->generateThumbName( $file->getName(), $params );
252
		$scalerThumbUrl = $scalerBaseUrl . '/' . $file->getUrlRel() .
253
			'/' . rawurlencode( $scalerThumbName );
254
255
		// make a curl call to the scaler to create a thumbnail
256
		$httpOptions = [
257
			'method' => 'GET',
258
			'timeout' => 5 // T90599 attempt to time out cleanly
259
		];
260
		$req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ );
261
		$status = $req->execute();
262
		if ( !$status->isOK() ) {
263
			$errors = $status->getErrorsArray();
264
			$errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
265
			$errorStr .= "\nurl = $scalerThumbUrl\n";
266
			throw new MWException( $errorStr );
267
		}
268
		$contentType = $req->getResponseHeader( "content-type" );
269
		if ( !$contentType ) {
270
			throw new MWException( "Missing content-type header" );
271
		}
272
273
		return $this->outputContents( $req->getContent(), $contentType );
274
	}
275
276
	/**
277
	 * Output HTTP response for file
278
	 * Side effect: writes HTTP response to STDOUT.
279
	 *
280
	 * @param File $file File object with a local path (e.g. UnregisteredLocalFile,
281
	 *   LocalFile. Oddly these don't share an ancestor!)
282
	 * @throws SpecialUploadStashTooLargeException
283
	 * @return bool
284
	 */
285
	private function outputLocalFile( File $file ) {
286
		if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
287
			throw new SpecialUploadStashTooLargeException();
288
		}
289
290
		return $file->getRepo()->streamFile( $file->getPath(),
0 ignored issues
show
It seems like $file->getPath() targeting File::getPath() can also be of type boolean; however, FileRepo::streamFile() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
291
			[ 'Content-Transfer-Encoding: binary',
292
				'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ]
293
		);
294
	}
295
296
	/**
297
	 * Output HTTP response of raw content
298
	 * Side effect: writes HTTP response to STDOUT.
299
	 * @param string $content Content
300
	 * @param string $contentType MIME type
301
	 * @throws SpecialUploadStashTooLargeException
302
	 * @return bool
303
	 */
304
	private function outputContents( $content, $contentType ) {
305
		$size = strlen( $content );
306
		if ( $size > self::MAX_SERVE_BYTES ) {
307
			throw new SpecialUploadStashTooLargeException();
308
		}
309
		// Cancel output buffering and gzipping if set
310
		wfResetOutputBuffers();
311
		self::outputFileHeaders( $contentType, $size );
312
		print $content;
313
314
		return true;
315
	}
316
317
	/**
318
	 * Output headers for streaming
319
	 * @todo Unsure about encoding as binary; if we received from HTTP perhaps
320
	 * we should use that encoding, concatenated with semicolon to `$contentType` as it
321
	 * usually is.
322
	 * Side effect: preps PHP to write headers to STDOUT.
323
	 * @param string $contentType String suitable for content-type header
324
	 * @param string $size Length in bytes
325
	 */
326
	private static function outputFileHeaders( $contentType, $size ) {
327
		header( "Content-Type: $contentType", true );
328
		header( 'Content-Transfer-Encoding: binary', true );
329
		header( 'Expires: Sun, 17-Jan-2038 19:14:07 GMT', true );
330
		// Bug 53032 - It shouldn't be a problem here, but let's be safe and not cache
331
		header( 'Cache-Control: private' );
332
		header( "Content-Length: $size", true );
333
	}
334
335
	/**
336
	 * Static callback for the HTMLForm in showUploads, to process
337
	 * Note the stash has to be recreated since this is being called in a static context.
338
	 * This works, because there really is only one stash per logged-in user, despite appearances.
339
	 *
340
	 * @param array $formData
341
	 * @param HTMLForm $form
342
	 * @return Status
343
	 */
344
	public static function tryClearStashedUploads( $formData, $form ) {
345
		if ( isset( $formData['Clear'] ) ) {
346
			$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $form->getUser() );
347
			wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" );
348
349
			if ( !$stash->clear() ) {
350
				return Status::newFatal( 'uploadstash-errclear' );
351
			}
352
		}
353
354
		return Status::newGood();
355
	}
356
357
	/**
358
	 * Default action when we don't have a subpage -- just show links to the uploads we have,
359
	 * Also show a button to clear stashed files
360
	 * @return bool
361
	 */
362
	private function showUploads() {
363
		// sets the title, etc.
364
		$this->setHeaders();
365
		$this->outputHeader();
366
367
		// create the form, which will also be used to execute a callback to process incoming form data
368
		// this design is extremely dubious, but supposedly HTMLForm is our standard now?
369
370
		$context = new DerivativeContext( $this->getContext() );
371
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
372
		$form = HTMLForm::factory( 'ooui', [
373
			'Clear' => [
374
				'type' => 'hidden',
375
				'default' => true,
376
				'name' => 'clear',
377
			]
378
		], $context, 'clearStashedUploads' );
379
		$form->setSubmitDestructive();
380
		$form->setSubmitCallback( [ __CLASS__, 'tryClearStashedUploads' ] );
381
		$form->setSubmitTextMsg( 'uploadstash-clear' );
382
383
		$form->prepareForm();
384
		$formResult = $form->tryAuthorizedSubmit();
385
386
		// show the files + form, if there are any, or just say there are none
387
		$refreshHtml = Html::element( 'a',
388
			[ 'href' => $this->getPageTitle()->getLocalURL() ],
389
			$this->msg( 'uploadstash-refresh' )->text() );
390
		$files = $this->stash->listFiles();
391
		if ( $files && count( $files ) ) {
392
			sort( $files );
393
			$fileListItemsHtml = '';
394
			$linkRenderer = $this->getLinkRenderer();
395
			foreach ( $files as $file ) {
396
				$itemHtml = $linkRenderer->makeKnownLink(
397
					$this->getPageTitle( "file/$file" ),
398
					$file
399
				);
400
				try {
401
					$fileObj = $this->stash->getFile( $file );
402
					$thumb = $fileObj->generateThumbName( $file, [ 'width' => 220 ] );
403
					$itemHtml .=
404
						$this->msg( 'word-separator' )->escaped() .
405
						$this->msg( 'parentheses' )->rawParams(
406
							$linkRenderer->makeKnownLink(
407
								$this->getPageTitle( "thumb/$file/$thumb" ),
408
								$this->msg( 'uploadstash-thumbnail' )->text()
409
							)
410
						)->escaped();
411
				} catch ( Exception $e ) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
412
				}
413
				$fileListItemsHtml .= Html::rawElement( 'li', [], $itemHtml );
414
			}
415
			$this->getOutput()->addHTML( Html::rawElement( 'ul', [], $fileListItemsHtml ) );
416
			$form->displayForm( $formResult );
417
			$this->getOutput()->addHTML( Html::rawElement( 'p', [], $refreshHtml ) );
418
		} else {
419
			$this->getOutput()->addHTML( Html::rawElement( 'p', [],
420
				Html::element( 'span', [], $this->msg( 'uploadstash-nofiles' )->text() )
421
				. ' '
422
				. $refreshHtml
423
			) );
424
		}
425
426
		return true;
427
	}
428
}
429
430
class SpecialUploadStashTooLargeException extends MWException {
431
}
432