Opened 5 weeks ago
Last modified 3 weeks ago
#65777 new defect (bug)
Login form aria-describedby ignores one of the two notices login_header() can render
| Reported by: | khokansardar | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Login and Registration | Version: | 6.1 |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: | accessibility |
Description (last modified by )
login_header() splits $errors into two separate notices by severity, and can render both at once:
#login_error— items whose error data is not'message'#login-message— items whose error data is'message'
The aria-describedby logic that wires those notices to the username and password fields assumes only one of them exists. When both render, one of them is left unreferenced: visible on screen, but never announced when focus reaches the input.
Affected code
src/wp-login.php, lines 1499–1508:
$aria_describedby = ''; $has_errors = $errors->has_errors(); if ( $has_errors ) { $aria_describedby = ' aria-describedby="login_error"'; } if ( $has_errors && 'message' === $errors->get_error_data() ) { $aria_describedby = ' aria-describedby="login-message"'; }
The value is consumed at lines 1517 (#user_login) and 1523 (#user_pass). The notices themselves are built in login_header(), at lines 243–312.
Root cause
Two independent defects in the same block:
get_error_data()is called with no argument. It falls back toget_error_code(), which returns only the first error code (src/wp-includes/class-wp-error.php, lines 155–161). The severity of every other item is ignored.- The second branch overwrites instead of appending.
aria-describedbyaccepts a space-separated list of IDs, but these two branches can only ever emit exactly one ID.
A failed sign-in populates $errors before the informational message is appended, so in practice the error-severity code is the first one, $aria_describedby sticks at login_error, and #login-message is left unreferenced. The logic is fragile in the other direction too: if a 'message'-severity code were ever the first one, the real error notice would be the one left unreferenced.
Steps to reproduce
- Load
wp-login.phpwithredirect_topointing atabout.php?updated, the address WordPress returns users to after a core update:wp-login.php?redirect_to=%2Fwp-admin%2Fabout.php%3Fupdated - Submit the form with invalid credentials.
- Inspect the rendered markup of
#user_loginand#user_pass.
This is reachable in core, without a plugin. wp_signon() puts an error-severity code into $errors, and wp-login.php:1444 then adds updated at 'message' severity to the same WP_Error object. $redirect_to survives the submission through the form's hidden redirect_to field, so both notices render on the response to the failed sign-in.
Actual
notices rendered : id="login_error" and id="login-message" aria-describedby : aria-describedby="login_error"
The message "You have successfully updated WordPress! Please log back in to see what's new." is rendered, but is never associated with either field.
Expected
aria-describedby="login_error login-message"
Change History (5)
This ticket was mentioned in PR #12796 on WordPress/wordpress-develop by @khokansardar.
5 weeks ago
#1
- Keywords has-unit-tests added
#2
@
5 weeks ago
@khokansardar thanks for the thorough report. I see the issue.
There's a few typos, incomplete sentences, and undesired line breaks that make the report a little difficult to parse. Could you please fix them, wnen you have a chance? If you don't have permission to edit, please add the whole text in a comment. I can later edit and delete the comment. Thank you.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
login_header()splits the login errors into a separate notice per severity and can display both at the same time. Thearia-describedbylogic assumed only one of them existed, so when both were displayed one notice was left unreferenced.What the problem was:
#login_error(errors) and#login-message(informational messages) can both render at once, but the association logic could only ever emit a single ID, so the other notice was visible on screen and never announced when focus reached the username or password field.WP_Error::get_error_data()was called without a code, so it fell back to the first error code and ignored the severity of every other item.What the fix does:
login_header()does, by passing each error code toWP_Error::get_error_data(), and references every notice that is actually displayed.Approach and why:
login_header()'s own partition means the IDs cannot drift from what is rendered.login_header()outputs the notices, so the order the descriptions are announced in matches the visual order regardless of error-code order.Trac ticket: https://core.trac.wordpress.org/ticket/65777
## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Ticket analysis, tests cases and generate ticket and PR. All changes were reviewed and validated by me.