PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.12
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.12
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | includes/trait-two-factor-session.php +117 -9 2.11.102.11.12 View file →
@@ -54,17 +54,24 @@
54 54 /**
55 55 * Whether this class is the one that must ask this account for its factor
56 56 *
57 57 * @since 2.11.10
58 + * @since 2.11.11 The enrolment can be passed in by a caller that has just read it.
58 59 *
59 - * @param WP_User $user User being authenticated.
60 - * @param string $method Method this class implements, 'email' or 'totp'.
60 + * @param WP_User $user User being authenticated.
61 + * @param string $method Method this class implements, 'email' or 'totp'.
62 + * @param bool|null $enrolled Whether the account has a TOTP enrolment, when the
63 + * caller already read its row. On a network that read
64 + * can search every site the account belongs to, and
65 + * the dashboard hooks run on every screen.
61 66 * @return bool
62 67 */
63 - protected function handles_second_factor( $user, $method ) {
64 - $enrolled = $this->database && method_exists( $this->database, 'has_totp_enrolment' )
65 - ? $this->database->has_totp_enrolment( $user->ID )
66 - : false;
68 + protected function handles_second_factor( $user, $method, $enrolled = null ) {
69 + if ( null === $enrolled ) {
70 + $enrolled = $this->database && method_exists( $this->database, 'has_totp_enrolment' )
71 + ? $this->database->has_totp_enrolment( $user->ID )
72 + : false;
73 + }
67 74
68 75 return ( $method === Vigilante_Settings::two_factor_handler_for( $user, $enrolled ) );
69 76 }
70 77
@@ -139,11 +146,76 @@
139 146 * Called from the module's init_hooks().
140 147 */
141 148 protected function init_session_hooks() {
142 149 add_action( 'application_password_did_authenticate', array( $this, 'remember_app_password_user' ) );
150 +
151 + // Why the verification session ended, explained on the login screen it
152 + // sends the visitor back to. Both classes use the trait, so both
153 + // register this; the notice itself prints once (see the method).
154 + add_filter( 'login_message', array( $this, 'show_2fa_session_notice' ) );
143 155 }
144 156
145 157 /**
158 + * Explain on the login screen why a verification session ended
159 + *
160 + * Until 2.11.12 running out of verification attempts cleared the pending
161 + * session and redirected to wp-login.php with no message at all: the visitor
162 + * was back at the password form with no idea why, typed the password again,
163 + * and that correct password was counted as one more failed login.
164 + *
165 + * The query argument only picks one of the literal strings below. It decides
166 + * nothing and it is not trusted for anything (rule 21): anyone can add it to
167 + * a URL, and all it can produce is one of these notices on a login screen.
168 + *
169 + * @since 2.11.12
170 + *
171 + * @param string $message Login screen message so far.
172 + * @return string
173 + */
174 + public function show_2fa_session_notice( $message ) {
175 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display of a static notice on the login screen; nothing is decided or written.
176 + $notice = isset( $_GET['vigilante_2fa_notice'] ) ? sanitize_key( wp_unslash( $_GET['vigilante_2fa_notice'] ) ) : '';
177 +
178 + if ( '' === $notice ) {
179 + return $message;
180 + }
181 +
182 + // Both two-factor classes use this trait and both register the filter,
183 + // so without this the notice would print twice on a site that has them
184 + // both loaded. A trait gives each using class its own statics, hence the
185 + // prefixed global rather than a static property.
186 + if ( ! empty( $GLOBALS['vigilante_2fa_notice_printed'] ) ) {
187 + return $message;
188 + }
189 +
190 + $texts = array(
191 + 'attempts' => __( 'Too many incorrect verification codes. The verification session was closed for security. Log in again to start a new one.', 'vigilante' ),
192 + 'expired' => __( 'The verification session expired. Log in again to start a new one.', 'vigilante' ),
193 + );
194 +
195 + if ( ! isset( $texts[ $notice ] ) ) {
196 + return $message;
197 + }
198 +
199 + $GLOBALS['vigilante_2fa_notice_printed'] = true;
200 +
201 + return $message . '<div id="login_error" class="notice notice-error"><p>' . esc_html( $texts[ $notice ] ) . '</p></div>';
202 + }
203 +
204 + /**
205 + * Send the visitor back to the login screen with an explanation
206 + *
207 + * @since 2.11.12
208 + *
209 + * @param string $notice One of the keys of show_2fa_session_notice().
210 + * @return void
211 + */
212 + private function redirect_to_login_with_notice( $notice ) {
213 + wp_safe_redirect( add_query_arg( 'vigilante_2fa_notice', rawurlencode( $notice ), wp_login_url() ) );
214 + exit;
215 + }
216 +
217 + /**
146 218 * Remember which user authenticated with an application password.
147 219 *
148 220 * @param WP_User $user Authenticated user.
149 221 */
@@ -190,8 +262,12 @@
190 262 *
191 263 * @return WP_Error
192 264 */
193 265 private function api_requires_2fa_error() {
266 + // A controlled rejection, not a wrong password: the credentials were
267 + // right and the account simply needs its second factor. The error code
268 + // is what keeps it out of the brute force count; see
269 + // Vigilante_Login_Security::CONTROLLED_REJECTIONS.
194 270 return new WP_Error(
195 271 'vigilante_2fa_required',
196 272 __( 'This account requires two-factor authentication. Log in from a browser, or use an application password for API access.', 'vigilante' )
197 273 );
@@ -224,14 +300,26 @@
224 300 // The attempt counter survives a fresh password login within the hour,
225 301 // so re-authenticating does not reset it (S2).
226 302 $attempts = ( is_array( $data ) && isset( $data['attempts'] ) ) ? absint( $data['attempts'] ) : 0;
227 303
304 + // Where the login was headed. The verification form is a second request
305 + // with its own POST, so redirect_to has to travel in the session or it
306 + // is lost and every login lands on the dashboard (2.11.12). Kept from a
307 + // previous pending session when this request does not carry one.
308 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a decision: stored as-is and validated against the site by wp_validate_redirect() before use, in pending_login_redirect().
309 + $redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : '';
310 +
311 + if ( '' === $redirect_to && is_array( $data ) && ! empty( $data['redirect_to'] ) ) {
312 + $redirect_to = (string) $data['redirect_to'];
313 + }
314 +
228 315 set_transient(
229 316 'vigilante_2fa_pending_' . $token,
230 317 array(
231 - 'user_id' => $user_id,
232 - 'created_at' => time(),
233 - 'attempts' => $attempts,
318 + 'user_id' => $user_id,
319 + 'created_at' => time(),
320 + 'attempts' => $attempts,
321 + 'redirect_to' => $redirect_to,
234 322 ),
235 323 HOUR_IN_SECONDS
236 324 );
237 325
@@ -321,8 +409,28 @@
321 409 private function get_pending_user_id() {
322 410 $session = $this->get_pending_session();
323 411
324 412 return $session ? $session['user_id'] : false;
413 + }
414 +
415 + /**
416 + * Where to send the visitor once the second factor is verified
417 + *
418 + * @since 2.11.12
419 + *
420 + * @return string URL on this site.
421 + */
422 + private function pending_login_redirect() {
423 + $session = $this->get_pending_session();
424 + $stored = ( is_array( $session ) && ! empty( $session['redirect_to'] ) ) ? (string) $session['redirect_to'] : '';
425 +
426 + if ( '' === $stored ) {
427 + return admin_url();
428 + }
429 +
430 + // Same gate core uses: anything off this site falls back to the
431 + // dashboard, so a stored value cannot send anyone off-site.
432 + return wp_validate_redirect( $stored, admin_url() );
325 433 }
326 434
327 435 /**
328 436 * Failed attempts recorded on the pending session presented by this request.