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/user/PasswordReset.php (3 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
 * User password reset helper for MediaWiki.
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
 */
22
23
use MediaWiki\Auth\AuthManager;
24
use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
25
26
/**
27
 * Helper class for the password reset functionality shared by the web UI and the API.
28
 *
29
 * Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
30
 * EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
31
 * functionality) to be enabled.
32
 */
33
class PasswordReset {
34
	/** @var Config */
35
	protected $config;
36
37
	/** @var AuthManager */
38
	protected $authManager;
39
40
	/**
41
	 * In-process cache for isAllowed lookups, by username. Contains pairs of StatusValue objects
42
	 * (for false and true value of $displayPassword, respectively).
43
	 * @var HashBagOStuff
44
	 */
45
	private $permissionCache;
46
47
	public function __construct( Config $config, AuthManager $authManager ) {
48
		$this->config = $config;
49
		$this->authManager = $authManager;
50
		$this->permissionCache = new HashBagOStuff( [ 'maxKeys' => 1 ] );
51
	}
52
53
	/**
54
	 * Check if a given user has permission to use this functionality.
55
	 * @param User $user
56
	 * @param bool $displayPassword If set, also check whether the user is allowed to reset the
57
	 *   password of another user and see the temporary password.
58
	 * @return StatusValue
59
	 */
60
	public function isAllowed( User $user, $displayPassword = false ) {
61
		$statuses = $this->permissionCache->get( $user->getName() );
62
		if ( $statuses ) {
63
			list ( $status, $status2 ) = $statuses;
64
		} else {
65
			$resetRoutes = $this->config->get( 'PasswordResetRoutes' );
66
			$status = StatusValue::newGood();
67
68
			if ( !is_array( $resetRoutes ) ||
69
				 !in_array( true, array_values( $resetRoutes ), true )
70
			) {
71
				// Maybe password resets are disabled, or there are no allowable routes
72
				$status = StatusValue::newFatal( 'passwordreset-disabled' );
73
			} elseif (
74
				( $providerStatus = $this->authManager->allowsAuthenticationDataChange(
75
					new TemporaryPasswordAuthenticationRequest(), false ) )
76
				&& !$providerStatus->isGood()
77
			) {
78
				// Maybe the external auth plugin won't allow local password changes
79
				$status = StatusValue::newFatal( 'resetpass_forbidden-reason',
80
					$providerStatus->getMessage() );
81
			} elseif ( !$this->config->get( 'EnableEmail' ) ) {
82
				// Maybe email features have been disabled
83
				$status = StatusValue::newFatal( 'passwordreset-emaildisabled' );
84
			} elseif ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
85
				// Maybe not all users have permission to change private data
86
				$status = StatusValue::newFatal( 'badaccess' );
87
			} elseif ( $user->isBlocked() ) {
88
				// Maybe the user is blocked (check this here rather than relying on the parent
89
				// method as we have a more specific error message to use here
90
				$status = StatusValue::newFatal( 'blocked-mailpassword' );
91
			}
92
93
			$status2 = StatusValue::newGood();
94
			if ( !$user->isAllowed( 'passwordreset' ) ) {
95
				$status2 = StatusValue::newFatal( 'badaccess' );
96
			}
97
98
			$this->permissionCache->set( $user->getName(), [ $status, $status2 ] );
99
		}
100
101
		if ( !$displayPassword || !$status->isGood() ) {
102
			return $status;
103
		} else {
104
			return $status2;
105
		}
106
	}
107
108
	/**
109
	 * Do a password reset. Authorization is the caller's responsibility.
110
	 *
111
	 * Process the form.  At this point we know that the user passes all the criteria in
112
	 * userCanExecute(), and if the data array contains 'Username', etc, then Username
113
	 * resets are allowed.
114
	 * @param User $performingUser The user that does the password reset
115
	 * @param string $username The user whose password is reset
116
	 * @param string $email Alternative way to specify the user
117
	 * @param bool $displayPassword Whether to display the password
118
	 * @return StatusValue Will contain the passwords as a username => password array if the
119
	 *   $displayPassword flag was set
120
	 * @throws LogicException When the user is not allowed to perform the action
121
	 * @throws MWException On unexpected DB errors
122
	 */
123
	public function execute(
124
		User $performingUser, $username = null, $email = null, $displayPassword = false
125
	) {
126
		if ( !$this->isAllowed( $performingUser, $displayPassword )->isGood() ) {
127
			$action = $this->isAllowed( $performingUser )->isGood() ? 'display' : 'reset';
128
			throw new LogicException( 'User ' . $performingUser->getName()
129
				. ' is not allowed to ' . $action . ' passwords' );
130
		}
131
132
		$resetRoutes = $this->config->get( 'PasswordResetRoutes' )
133
			+ [ 'username' => false, 'email' => false ];
134
		if ( $resetRoutes['username'] && $username ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $username of type string|null 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...
135
			$method = 'username';
136
			$users = [ User::newFromName( $username ) ];
137
		} elseif ( $resetRoutes['email'] && $email ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $email of type string|null 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...
138
			if ( !Sanitizer::validateEmail( $email ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Sanitizer::validateEmail($email) of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
139
				return StatusValue::newFatal( 'passwordreset-invalidemail' );
140
			}
141
			$method = 'email';
142
			$users = $this->getUsersByEmail( $email );
143
		} else {
144
			// The user didn't supply any data
145
			return StatusValue::newFatal( 'passwordreset-nodata' );
146
		}
147
148
		// Check for hooks (captcha etc), and allow them to modify the users list
149
		$error = [];
150
		$data = [
151
			'Username' => $username,
152
			'Email' => $email,
153
			'Capture' => $displayPassword ? '1' : null,
154
		];
155
		if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
156
			return StatusValue::newFatal( Message::newFromSpecifier( $error ) );
157
		}
158
159
		if ( !$users ) {
160
			if ( $method === 'email' ) {
161
				// Don't reveal whether or not an email address is in use
162
				return StatusValue::newGood( [] );
163
			} else {
164
				return StatusValue::newFatal( 'noname' );
165
			}
166
		}
167
168
		$firstUser = $users[0];
169
170
		if ( !$firstUser instanceof User || !$firstUser->getId() ) {
171
			// Don't parse username as wikitext (bug 65501)
172
			return StatusValue::newFatal( wfMessage( 'nosuchuser', wfEscapeWikiText( $username ) ) );
173
		}
174
175
		// Check against the rate limiter
176
		if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
177
			return StatusValue::newFatal( 'actionthrottledtext' );
178
		}
179
180
		// All the users will have the same email address
181
		if ( !$firstUser->getEmail() ) {
182
			// This won't be reachable from the email route, so safe to expose the username
183
			return StatusValue::newFatal( wfMessage( 'noemail',
184
				wfEscapeWikiText( $firstUser->getName() ) ) );
185
		}
186
187
		// We need to have a valid IP address for the hook, but per bug 18347, we should
188
		// send the user's name if they're logged in.
189
		$ip = $performingUser->getRequest()->getIP();
190
		if ( !$ip ) {
191
			return StatusValue::newFatal( 'badipaddress' );
192
		}
193
194
		Hooks::run( 'User::mailPasswordInternal', [ &$performingUser, &$ip, &$firstUser ] );
195
196
		$result = StatusValue::newGood();
197
		$reqs = [];
198
		foreach ( $users as $user ) {
199
			$req = TemporaryPasswordAuthenticationRequest::newRandom();
200
			$req->username = $user->getName();
201
			$req->mailpassword = true;
202
			$req->hasBackchannel = $displayPassword;
203
			$req->caller = $performingUser->getName();
204
			$status = $this->authManager->allowsAuthenticationDataChange( $req, true );
205
			if ( $status->isGood() && $status->getValue() !== 'ignored' ) {
206
				$reqs[] = $req;
207
			} elseif ( $result->isGood() ) {
208
				// only record the first error, to avoid exposing the number of users having the
209
				// same email address
210
				if ( $status->getValue() === 'ignored' ) {
211
					$status = StatusValue::newFatal( 'passwordreset-ignored' );
212
				}
213
				$result->merge( $status );
214
			}
215
		}
216
217
		if ( !$result->isGood() ) {
218
			return $result;
219
		}
220
221
		$passwords = [];
222
		foreach ( $reqs as $req ) {
223
			$this->authManager->changeAuthenticationData( $req );
224
			// TODO record mail sending errors
225
			if ( $displayPassword ) {
226
				$passwords[$req->username] = $req->password;
227
			}
228
		}
229
230
		return StatusValue::newGood( $passwords );
231
	}
232
233
	/**
234
	 * @param string $email
235
	 * @return User[]
236
	 * @throws MWException On unexpected database errors
237
	 */
238
	protected function getUsersByEmail( $email ) {
239
		$res = wfGetDB( DB_REPLICA )->select(
240
			'user',
241
			User::selectFields(),
242
			[ 'user_email' => $email ],
243
			__METHOD__
244
		);
245
246
		if ( !$res ) {
247
			// Some sort of database error, probably unreachable
248
			throw new MWException( 'Unknown database error in ' . __METHOD__ );
249
		}
250
251
		$users = [];
252
		foreach ( $res as $row ) {
253
			$users[] = User::newFromRow( $row );
254
		}
255
		return $users;
256
	}
257
}
258