Skip to content

Application Passwords: verify Basic Auth with the server before blocking. - #13597

Open
Aayushkalikote wants to merge 1 commit into
WordPress:trunkfrom
Aayushkalikote:fix/66000
Open

Aayushkalikote wants to merge 1 commit into
WordPress:trunkfrom
Aayushkalikote:fix/66000

Conversation

@Aayushkalikote

Copy link
Copy Markdown

Trac ticket: https://core.trac.wordpress.org/ticket/66000

The problem

wp_is_site_protected_by_basic_auth() treats the presence of PHP_AUTH_USER / PHP_AUTH_PW as proof that the server enforces HTTP Basic Auth:

$is_protected = ! empty( $_SERVER['PHP_AUTH_USER'] ) || ! empty( $_SERVER['PHP_AUTH_PW'] );

Under the CGI/FastCGI SAPI, PHP populates those keys from any Authorization: Basic header the client chooses to send — no server-side Basic Auth needs to be configured for them to appear. Browsers keep replaying cached credentials to an origin for the rest of the session, so after Basic Auth is switched off the affected browser keeps seeing:

Your website appears to use Basic Authentication, which is not currently compatible with Application Passwords.

while a fresh browser or private window works normally. That notice hides the form in wp-admin/user-edit.php and hard-fails wp-admin/authorize-application.php with a 501.

As noted on the ticket, the two cases are indistinguishable from within the request — AUTH_TYPE and REMOTE_USER are unset for both a genuinely protected site and a stale credential, so neither can discriminate.

The change

The distinction does exist on the wire: a server that enforces Basic Auth must answer an unauthenticated request with 401 and a WWW-Authenticate: Basic challenge, and a browser replaying cached credentials cannot produce that. This is the loopback check [50006] named as the intended robust evaluation when the function was introduced for #52066.

wp_is_site_protected_by_basic_auth() keeps its existing credential check. When credentials are present, it now confirms with the server through a new private helper, wp_is_basic_auth_enforced_by_server(): an unauthenticated wp_remote_head() to home_url( '/' ) looking for a 401 plus a Basic challenge.

  • No cost for unaffected sites. The check is gated behind the existing credential check, so a request carrying no Basic Auth credentials never triggers it. Behaviour for those sites is unchanged.
  • Cached. The result is stored in the wp_basic_auth_enforced transient for 15 minutes. The lifetime is deliberately short: the server's configuration can change at any time, and a long-lived cache reproduces this ticket's own symptom until it expires.
  • Fails closed. If the loopback request errors, the function returns true, exactly as before this check existed, and the failure is not cached so the next request can retry. Sites behind Basic Auth with loopback requests blocked see no change, so the conflict #52066 was opened to prevent stays prevented.
  • The filter is untouched. wp_is_site_protected_by_basic_auth still short-circuits everything, so the documented __return_false workaround keeps working.

Tests

wp_is_site_protected_by_basic_auth() had no PHPUnit coverage at all. tests/phpunit/tests/load/wpIsSiteProtectedByBasicAuth.php adds it, mocking the loopback request through pre_http_request:

  • no credentials present, and no loopback request made in that case
  • each credential key present individually, and empty values
  • stale credentials with a 200 response → false (this ticket)
  • 401 with a non-Basic challenge → false
  • 401 with a Basic challenge → true
  • loopback failure → true, and not cached
  • the transient limits the check to a single request
  • the wp_is_site_protected_by_basic_auth filter overriding in both directions
  • context auto-detection on the login screen

Testing instructions

npm run test:php -- --filter Tests_Load_wpIsSiteProtectedByBasicAuth

Manual, reproducing the original report:

  1. Enable HTTP Basic Auth on the server. Load Users → Profile → Application Passwords. The warning appears — correct.
  2. Disable Basic Auth. Reload the profile screen in the same browser session. On trunk the warning persists and the form stays hidden; with this change the form is available again.
  3. Re-enable Basic Auth, delete the wp_basic_auth_enforced transient, and reload. The warning returns.
  4. A second browser or private window behaves the same in both directions.

Verified on trunk, PHP 8.5 (fpm-fcgi), nginx 1.31.5:

Case Before After
No credentials in the request false false
Authorization: Basic for a credential the server has never seen, no Basic Auth configured true false
nginx auth_basic enabled, valid credentials true true

The middle row is this ticket. The last row is the #52066 behaviour, unchanged.

Open questions

  1. The transient name and the 15 minute lifetime are my choice — happy to change either. There is no hook for "the server's Basic Auth configuration changed", so a bounded lifetime seemed the honest trade-off.
  2. Failing closed on a loopback error is the conservative option, but it does mean a site with loopback requests blocked keeps seeing the current behaviour. If preferred, that case could be surfaced in Site Health instead.

Props ethicaladitya, khokansardar, smeunus.

…ing.

The presence of PHP_AUTH_USER or PHP_AUTH_PW is not proof that the server
enforces HTTP Basic Auth. Under the CGI/FastCGI SAPI, PHP populates those
keys from any Authorization header the client sends, and browsers keep
replaying cached credentials for the rest of the session, so the
Application Passwords warning persists after Basic Auth is disabled.

Confirm with the server via an unauthenticated loopback request when
credentials are present, looking for a 401 with a Basic challenge. The
check is skipped entirely when no credentials are present, the result is
cached in a short-lived transient, and a failed loopback request
preserves the previous behaviour.

Adds test coverage for wp_is_site_protected_by_basic_auth(), which had
none.

Props ethicaladitya, khokansardar, smeunus.
Fixes #66000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props aayushk777.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

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