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/session/PHPSessionHandler.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
 * Session storage in object cache.
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 Session
22
 */
23
24
namespace MediaWiki\Session;
25
26
use Psr\Log\LoggerInterface;
27
use BagOStuff;
28
29
/**
30
 * Adapter for PHP's session handling
31
 * @ingroup Session
32
 * @since 1.27
33
 */
34
class PHPSessionHandler implements \SessionHandlerInterface {
35
	/** @var PHPSessionHandler */
36
	protected static $instance = null;
37
38
	/** @var bool Whether PHP session handling is enabled */
39
	protected $enable = false;
40
	protected $warn = true;
41
42
	/** @var SessionManager|null */
43
	protected $manager;
44
45
	/** @var BagOStuff|null */
46
	protected $store;
47
48
	/** @var LoggerInterface */
49
	protected $logger;
50
51
	/** @var array Track original session fields for later modification check */
52
	protected $sessionFieldCache = [];
53
54
	protected function __construct( SessionManager $manager ) {
55
		$this->setEnableFlags(
56
			\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
57
		);
58
		$manager->setupPHPSessionHandler( $this );
59
	}
60
61
	/**
62
	 * Set $this->enable and $this->warn
63
	 *
64
	 * Separate just because there doesn't seem to be a good way to test it
65
	 * otherwise.
66
	 *
67
	 * @param string $PHPSessionHandling See $wgPHPSessionHandling
68
	 */
69
	private function setEnableFlags( $PHPSessionHandling ) {
70
		switch ( $PHPSessionHandling ) {
71
			case 'enable':
72
				$this->enable = true;
73
				$this->warn = false;
74
				break;
75
76
			case 'warn':
77
				$this->enable = true;
78
				$this->warn = true;
79
				break;
80
81
			case 'disable':
82
				$this->enable = false;
83
				$this->warn = false;
84
				break;
85
		}
86
	}
87
88
	/**
89
	 * Test whether the handler is installed
90
	 * @return bool
91
	 */
92
	public static function isInstalled() {
93
		return (bool)self::$instance;
94
	}
95
96
	/**
97
	 * Test whether the handler is installed and enabled
98
	 * @return bool
99
	 */
100
	public static function isEnabled() {
101
		return self::$instance && self::$instance->enable;
102
	}
103
104
	/**
105
	 * Install a session handler for the current web request
106
	 * @param SessionManager $manager
107
	 */
108
	public static function install( SessionManager $manager ) {
109
		if ( self::$instance ) {
110
			$manager->setupPHPSessionHandler( self::$instance );
111
			return;
112
		}
113
114
		// @codeCoverageIgnoreStart
115
		if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
116
			throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
117
		}
118
		// @codeCoverageIgnoreEnd
119
120
		self::$instance = new self( $manager );
121
122
		// Close any auto-started session, before we replace it
123
		session_write_close();
124
125
		// Tell PHP not to mess with cookies itself
126
		ini_set( 'session.use_cookies', 0 );
127
		ini_set( 'session.use_trans_sid', 0 );
128
129
		// T124510: Disable automatic PHP session related cache headers.
130
		// MediaWiki adds it's own headers and the default PHP behavior may
131
		// set headers such as 'Pragma: no-cache' that cause problems with
132
		// some user agents.
133
		session_cache_limiter( '' );
134
135
		// Also set a sane serialization handler
136
		\Wikimedia\PhpSessionSerializer::setSerializeHandler();
137
138
		// Register this as the save handler, and register an appropriate
139
		// shutdown function.
140
		session_set_save_handler( self::$instance, true );
141
	}
142
143
	/**
144
	 * Set the manager, store, and logger
145
	 * @private Use self::install().
146
	 * @param SessionManager $manager
147
	 * @param BagOStuff $store
148
	 * @param LoggerInterface $store
149
	 */
150
	public function setManager(
151
		SessionManager $manager, BagOStuff $store, LoggerInterface $logger
152
	) {
153
		if ( $this->manager !== $manager ) {
154
			// Close any existing session before we change stores
155
			if ( $this->manager ) {
156
				session_write_close();
157
			}
158
			$this->manager = $manager;
159
			$this->store = $store;
160
			$this->logger = $logger;
161
			\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
162
		}
163
	}
164
165
	/**
166
	 * Workaround for PHP5 bug
167
	 *
168
	 * PHP5 has a bug in handling boolean return values for
169
	 * SessionHandlerInterface methods, it expects 0 or -1 instead of true or
170
	 * false. See <https://wiki.php.net/rfc/session.user.return-value>.
171
	 *
172
	 * PHP7 and HHVM are not affected.
173
	 *
174
	 * @todo When we drop support for Zend PHP 5, this can be removed.
175
	 * @return bool|int
176
	 * @codeCoverageIgnore
177
	 */
178
	protected static function returnSuccess() {
179
		return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? true : 0;
180
	}
181
182
	/**
183
	 * Workaround for PHP5 bug
184
	 * @see self::returnSuccess()
185
	 * @return bool|int
186
	 * @codeCoverageIgnore
187
	 */
188
	protected static function returnFailure() {
189
		return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? false : -1;
190
	}
191
192
	/**
193
	 * Initialize the session (handler)
194
	 * @private For internal use only
195
	 * @param string $save_path Path used to store session files (ignored)
196
	 * @param string $session_name Session name (ignored)
197
	 * @return bool|int Success (see self::returnSuccess())
198
	 */
199
	public function open( $save_path, $session_name ) {
200
		if ( self::$instance !== $this ) {
201
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
202
		}
203
		if ( !$this->enable ) {
204
			throw new \BadMethodCallException( 'Attempt to use PHP session management' );
205
		}
206
		return self::returnSuccess();
207
	}
208
209
	/**
210
	 * Close the session (handler)
211
	 * @private For internal use only
212
	 * @return bool|int Success (see self::returnSuccess())
213
	 */
214
	public function close() {
215
		if ( self::$instance !== $this ) {
216
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
217
		}
218
		$this->sessionFieldCache = [];
219
		return self::returnSuccess();
220
	}
221
222
	/**
223
	 * Read session data
224
	 * @private For internal use only
225
	 * @param string $id Session id
226
	 * @return string Session data
227
	 */
228
	public function read( $id ) {
229
		if ( self::$instance !== $this ) {
230
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
231
		}
232
		if ( !$this->enable ) {
233
			throw new \BadMethodCallException( 'Attempt to use PHP session management' );
234
		}
235
236
		$session = $this->manager->getSessionById( $id, false );
237
		if ( !$session ) {
238
			return '';
239
		}
240
		$session->persist();
241
242
		$data = iterator_to_array( $session );
243
		$this->sessionFieldCache[$id] = $data;
244
		return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
245
	}
246
247
	/**
248
	 * Write session data
249
	 * @private For internal use only
250
	 * @param string $id Session id
251
	 * @param string $dataStr Session data. Not that you should ever call this
252
	 *   directly, but note that this has the same issues with code injection
253
	 *   via user-controlled data as does PHP's unserialize function.
254
	 * @return bool|int Success (see self::returnSuccess())
255
	 */
256
	public function write( $id, $dataStr ) {
257
		if ( self::$instance !== $this ) {
258
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
259
		}
260
		if ( !$this->enable ) {
261
			throw new \BadMethodCallException( 'Attempt to use PHP session management' );
262
		}
263
264
		$session = $this->manager->getSessionById( $id, true );
265
		if ( !$session ) {
266
			// This can happen under normal circumstances, if the session exists but is
267
			// invalid. Let's emit a log warning instead of a PHP warning.
268
			$this->logger->warning(
269
				__METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
270
				[
271
					'session' => $id,
272
			] );
273
			return self::returnSuccess();
274
		}
275
276
		// First, decode the string PHP handed us
277
		$data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
278
		if ( $data === null ) {
279
			// @codeCoverageIgnoreStart
280
			return self::returnFailure();
281
			// @codeCoverageIgnoreEnd
282
		}
283
284
		// Now merge the data into the Session object.
285
		$changed = false;
286
		$cache = isset( $this->sessionFieldCache[$id] ) ? $this->sessionFieldCache[$id] : [];
287
		foreach ( $data as $key => $value ) {
288
			if ( !array_key_exists( $key, $cache ) ) {
289 View Code Duplication
				if ( $session->exists( $key ) ) {
290
					// New in both, so ignore and log
291
					$this->logger->warning(
292
						__METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
293
					);
294
				} else {
295
					// New in $_SESSION, keep it
296
					$session->set( $key, $value );
297
					$changed = true;
298
				}
299
			} elseif ( $cache[$key] === $value ) {
0 ignored issues
show
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
300
				// Unchanged in $_SESSION, so ignore it
301 View Code Duplication
			} elseif ( !$session->exists( $key ) ) {
302
				// Deleted in Session, keep but log
303
				$this->logger->warning(
304
					__METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
305
				);
306
				$session->set( $key, $value );
307
				$changed = true;
308
			} elseif ( $cache[$key] === $session->get( $key ) ) {
309
				// Unchanged in Session, so keep it
310
				$session->set( $key, $value );
311
				$changed = true;
312
			} else {
313
				// Changed in both, so ignore and log
314
				$this->logger->warning(
315
					__METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
316
				);
317
			}
318
		}
319
		// Anything deleted in $_SESSION and unchanged in Session should be deleted too
320
		// (but not if $_SESSION can't represent it at all)
321
		\Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
322
		foreach ( $cache as $key => $value ) {
323
			if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
324
				\Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
0 ignored issues
show
Bug Best Practice introduced by
The expression \Wikimedia\PhpSessionSer...de(array($key => true)) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
325
			) {
326
				if ( $cache[$key] === $session->get( $key ) ) {
327
					// Unchanged in Session, delete it
328
					$session->remove( $key );
329
					$changed = true;
330
				} else {
331
					// Changed in Session, ignore deletion and log
332
					$this->logger->warning(
333
						__METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
334
					);
335
				}
336
			}
337
		}
338
		\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
339
340
		// Save and update cache if anything changed
341
		if ( $changed ) {
342
			if ( $this->warn ) {
343
				wfDeprecated( '$_SESSION', '1.27' );
344
				$this->logger->warning( 'Something wrote to $_SESSION!' );
345
			}
346
347
			$session->save();
348
			$this->sessionFieldCache[$id] = iterator_to_array( $session );
349
		}
350
351
		$session->persist();
352
353
		return self::returnSuccess();
354
	}
355
356
	/**
357
	 * Destroy a session
358
	 * @private For internal use only
359
	 * @param string $id Session id
360
	 * @return bool|int Success (see self::returnSuccess())
361
	 */
362
	public function destroy( $id ) {
363
		if ( self::$instance !== $this ) {
364
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
365
		}
366
		if ( !$this->enable ) {
367
			throw new \BadMethodCallException( 'Attempt to use PHP session management' );
368
		}
369
		$session = $this->manager->getSessionById( $id, false );
370
		if ( $session ) {
371
			$session->clear();
372
		}
373
		return self::returnSuccess();
374
	}
375
376
	/**
377
	 * Execute garbage collection.
378
	 * @private For internal use only
379
	 * @param int $maxlifetime Maximum session life time (ignored)
380
	 * @return bool|int Success (see self::returnSuccess())
381
	 * @codeCoverageIgnore See T135576
382
	 */
383
	public function gc( $maxlifetime ) {
384
		if ( self::$instance !== $this ) {
385
			throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
386
		}
387
		$before = date( 'YmdHis', time() );
388
		$this->store->deleteObjectsExpiringBefore( $before );
389
		return self::returnSuccess();
390
	}
391
}
392