This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
|
|||
135 | $method = 'username'; |
||
136 | $users = [ User::newFromName( $username ) ]; |
||
137 | } elseif ( $resetRoutes['email'] && $email ) { |
||
0 ignored issues
–
show
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 For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
138 | if ( !Sanitizer::validateEmail( $email ) ) { |
||
0 ignored issues
–
show
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 $a = canBeFalseAndNull();
// Instead of
if ( ! $a) { }
// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
![]() |
|||
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 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
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: