PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
← All changes | src/DonorDashboards/Routes/LoginRoute.php +75 -31 4.15.2 → 4.18.0 View file →
@@ -45,16 +45,29 @@
45 45 );
46 46 }
47 47
48 48 /**
49 - * Handles login request
49 + * Handles login request.
50 50 *
51 + * Authentication is delegated to wp_signon() so that the request runs
52 + * through WordPress's `authenticate` filter chain. This lets brute-force
53 + * protection plugins (Wordfence, Login LockDown, Jetpack Protect, Solid
54 + * Security, etc.) intercept and block attempts, and ensures the core
55 + * `wp_login_failed` action fires on failure so plugins that hook it (Limit
56 + * Login Attempts, etc.) can count the attempt. Core authentication failures
57 + * (wrong password, unknown username/email) return a single generic response
58 + * so the endpoint cannot be used to enumerate valid accounts. A lockout from
59 + * a protection plugin is surfaced as a 429 with that plugin's message so a
60 + * rate-limited user understands why the login was refused.
61 + *
62 + * @since 4.15.5 Route through wp_signon() and return a generic failure
63 + * response to prevent brute-force-protection bypass (CWE-307)
64 + * and user enumeration (CWE-204); surface lockout messages as 429.
51 65 * @since 2.10.0
52 66 *
53 67 * @param WP_REST_Request $request
54 68 *
55 - * @return array
56 - *
69 + * @return WP_REST_Response
57 70 */
58 71 public function handleRequest(WP_REST_Request $request)
59 72 {
60 73 $login = $request->get_param('login');
@@ -59,51 +72,82 @@
59 72 {
60 73 $login = $request->get_param('login');
61 74 $password = $request->get_param('password');
62 75
63 - $user = get_user_by('login', $login);
76 + // wp_signon() runs the `authenticate` filter chain (so lockout plugins
77 + // can intercept and block), fires `wp_login_failed` on failure, and sets
78 + // the auth cookie on success.
79 + $user = wp_signon(
80 + [
81 + 'user_login' => $login,
82 + 'user_password' => $password,
83 + 'remember' => false,
84 + ]
85 + );
64 86
65 - if ( ! $user) {
66 - $user = get_user_by('email', $login);
67 - }
87 + if (is_wp_error($user)) {
88 + // Core authentication errors (wrong password, unknown username/email)
89 + // are collapsed into a single generic message so the endpoint cannot
90 + // be used to enumerate accounts. Any other error code originates from
91 + // a brute-force protection plugin (lockout) — surface its message so
92 + // a locked-out user understands why the login was refused.
93 + $coreAuthErrorCodes = [
94 + 'incorrect_password',
95 + 'invalid_username',
96 + 'invalid_email',
97 + 'empty_username',
98 + 'empty_password',
99 + ];
68 100
69 - if ($user) {
70 - if (wp_check_password($password, $user->user_pass, $user->ID)) {
71 - give_log_user_in($user->ID, $login, $password);
101 + $isLockout = ! in_array($user->get_error_code(), $coreAuthErrorCodes, true);
72 102
103 + // Note: the logical result is returned in the body `status` field and
104 + // the HTTP status is left at 200, matching the endpoint's original
105 + // contract. The Donor Dashboard front-end reads the result from the
106 + // body; returning a non-2xx HTTP status would make its Axios request
107 + // reject and leave the form stuck in a loading state.
108 + if ($isLockout) {
109 + $lockoutMessage = wp_strip_all_tags($user->get_error_message());
110 +
73 111 return new WP_REST_Response(
74 112 [
75 - 'status' => 200,
76 - 'response' => 'login_successful',
113 + 'status' => 429,
114 + 'response' => 'too_many_attempts',
77 115 'body_response' => [
78 - 'login' => $user->login,
79 - 'id' => $user->ID,
116 + 'message' => '' !== $lockoutMessage
117 + ? $lockoutMessage
118 + : __('Too many failed login attempts. Please try again later.', 'give'),
80 119 ],
81 120 ]
82 121 );
83 - } else {
84 - return new WP_REST_Response(
85 - [
86 - 'status' => 400,
87 - 'response' => 'incorrect_password',
88 - 'body_response' => [
89 - 'message' => __('The provided password was incorrect.', 'give'),
90 - ],
91 - ]
92 - );
93 122 }
94 - } else {
123 +
95 124 return new WP_REST_Response(
96 125 [
97 - 'status' => 400,
98 - 'response' => 'unidentified_login',
126 + 'status' => 401,
127 + 'response' => 'login_failed',
99 128 'body_response' => [
100 - 'message' => sprintf(
101 - __('A record for the provided login (%s) could not be found.', 'give'),
102 - $login
103 - ),
129 + 'message' => __('The provided credentials are invalid.', 'give'),
104 130 ],
105 131 ]
106 132 );
107 133 }
134 +
135 + // wp_signon() sets the auth cookie and fires `wp_login`, but does not set
136 + // the current user for the active request. Set it so the rest of this
137 + // request is authenticated, then fire only GiveWP's own hook so existing
138 + // integrations keep working, without duplicating the cookie or `wp_login`.
139 + wp_set_current_user($user->ID, $user->user_login);
140 + do_action('give_log_user_in', $user->ID, $user->user_login, $password);
141 +
142 + return new WP_REST_Response(
143 + [
144 + 'status' => 200,
145 + 'response' => 'login_successful',
146 + 'body_response' => [
147 + 'login' => $user->user_login,
148 + 'id' => $user->ID,
149 + ],
150 + ]
151 + );
108 152 }
109 153 }