Skip to content

fix(security): harden the front-end AJAX handlers (superseded by #150) - #149

Closed
eleshar wants to merge 2 commits into
chore/lsx-login-gulp5from
fix/lsx-login-nopriv-ajax
Closed

eleshar wants to merge 2 commits into
chore/lsx-login-gulp5from
fix/lsx-login-nopriv-ajax

Conversation

@eleshar

@eleshar eleshar commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by #150.

This pull request was closed automatically when its base branch was deleted on merge of #148.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 248377bc-84ac-4e56-8422-d0929b436953

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…onse

do_ajax_reset() is registered on both wp_ajax_lsx_reset and
wp_ajax_nopriv_lsx_reset, so it is reachable unauthenticated, and it takes
no nonce and performs no capability check.

It generated a password reset key, stored the hash against the user, built
the reset email body containing

  wp-login.php?action=rp&key=$key&login=<user_login>

and then assigned that whole body to $result['email'], which was echoed
back to the caller as JSON.

Any unauthenticated visitor could therefore POST to admin-ajax.php with
action=lsx_reset, method=reset and log=<any username or email> and read the
reset link straight out of the response, then complete the reset through
lsx_reset_confirmed - which is also nopriv and validates only the key.
That is unauthenticated account takeover of any user on the site,
administrators included.

The email body is dropped from the response. The front-end never read
result.email - assets/js/src/lsx-login.js only uses result.success and
result.message - so nothing depended on it.

This does not add the missing nonce and rate limiting on the nopriv
handlers; that needs the front-end to pass a nonce and is tracked
separately.

Also escaping output on the same reviewed paths:

- templates/content-password-reset.php echoed $_GET['login'] and
  $_GET['key'] straight into hidden input values, giving reflected XSS on
  the password reset screen. Both now go through sanitize_user /
  sanitize_text_field, wp_unslash and esc_attr.
- class-lsx-login.php interpolated $_POST['log'] into a __() call, which
  both broke the translation lookup and passed unsanitised input into the
  response. It is now a sprintf() with a placeholder and an escaped,
  sanitised argument.
lsx_login, lsx_reset and lsx_reset_confirmed are all registered on
wp_ajax_nopriv_*, so they are reachable unauthenticated. None of them
checked a nonce, so any origin could drive them directly.

- wp_localize_script now passes a nonce for the lsx_login_ajax action, and
  all three requests in assets/js/src/lsx-login.js send it.
- Each handler calls verify_ajax_nonce() first, which responds 403 with a
  "session has expired, reload" message and stops on failure.
- do_ajax_reset() is additionally throttled to 5 requests per IP per 15
  minutes via a transient. Without it an unauthenticated caller could
  invalidate any user's stored activation key, and mail them, in a loop.
- do_ajax_reset_confirmed() now compares pass1 and pass2 server-side. The
  check existed only in the front-end, so a mismatch it failed to catch
  silently set pass1 as the new password.

Note for cached sites: the nonce is rendered into the page, so a full-page
cache serving a login form older than the nonce lifetime will produce 403s
until the page is regenerated. Exclude the login page from full-page
caching, or shorten its TTL below the nonce tick.
@eleshar
eleshar force-pushed the fix/lsx-login-nopriv-ajax branch from cadcdef to 2a27423 Compare September 8, 2026 10:32
@eleshar
eleshar deleted the branch chore/lsx-login-gulp5 September 8, 2026 10:49
@eleshar eleshar closed this Sep 8, 2026
@eleshar eleshar changed the title fix(security): unauthenticated account takeover via the password reset AJAX handler fix(security): harden the front-end AJAX handlers (superseded by #150) Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant