Opened 29 hours ago
Last modified 3 hours ago
#66380 reviewing defect (bug)
Secrets API: Site Health misreports the secrets backend and gives key advice that locks stored secrets
| Reported by: | nofearinc | Owned by: | ericmann |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Security | Version: | trunk |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description
Follow-up to [64337] (#66187), found while testing 7.2-beta1.
1. Site Health advice that locks every stored secret. On a site without WP_SECRETS_KEY, the key source test says to "Define WP_SECRETS_KEY with a value from wp secret generate-key". That command is not in core or WP-CLI (the commands stayed in the feature plugin, per [64337]). Following the advice after any secret is stored makes every stored secret unreadable and blocks saving new ones, because the root key stays wrapped with the previous site key and nothing in core re-wraps it (rotate_site_key() has no caller). Site Health then reports the new key as good.
Reproduced on trunk:
wp_set_secret( 'test/api-key', 'value' ), read back fine. Site Health: recommended, "Define WP_SECRETS_KEY with a value fromwp secret generate-key".- Define
WP_SECRETS_KEYas a base64 random 32-byte value:wp_get_secret()returnssecret_key_unavailable,wp_set_secret()fails too, and the key source test says "Secrets API is using a dedicated key" (good). - Remove the constant: the secret reads back again.
This is the Site Health side of the risk @Clorith raised in #66187 comment:23 for salt rotation. It does not change the salt fallback itself.
2. A broken backend reads as healthy. The undecryptable and rotation tests turn the WP_Error from wp_list_secrets() into an empty result ("All secrets can be decrypted", "No secrets are pending rotation"). The key source test detects a broken secrets.php by matching "broken" in a translated string, so on translated sites it reports "using a dedicated key".
3. Provider drop-ins get the wrong advice. With a secrets.php that replaces the provider (a host KMS or vault), the key source test still describes the unused default keyring and recommends WP_SECRETS_KEY.
4. secrets.php is not listed as a drop-in. It loads on every request like object-cache.php, but _get_dropins() does not include it, so neither Plugins > Drop-ins nor Site Health Info shows it.
PRs: https://github.com/WordPress/wordpress-develop/pull/14847 (1 to 3, one commit each) and https://github.com/WordPress/wordpress-develop/pull/14848 (4).
AI assistance: Claude Code (Claude Opus 5.5) for finding these, drafting the patches and tests, and running them. I reviewed the patches and reproduced the steps above on 7.2-beta1.
Change History (5)
This ticket was mentioned in PR #14847 on WordPress/wordpress-develop by @nofearinc.
29 hours ago
#1
This ticket was mentioned in PR #14848 on WordPress/wordpress-develop by @nofearinc.
29 hours ago
#2
wp-content/secrets.php is loaded on every request since [64337], like object-cache.php, and can replace how every stored credential is protected, but it was missing from _get_dropins(). Neither the Drop-ins view of the Plugins screen nor the Drop-ins section of Site Health Info showed it.
Testing
vendor/bin/phpunit --filter test_get_dropins_includes_secrets_dropinfails without the patch and passes with it.vendor/bin/phpunit --filter Tests_Admin_IncludesPluginpasses (65 tests).
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: finding the issue, drafting the fix and test, and running them. I reviewed every line.
---
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
🤖 Generated with Claude Code
#5
@
3 hours ago
Thanks @nofearinc, this is a solid report, and the PRs are in good shape.
I reproduced all four parts on a clean 7.2-beta1 with no plugins. Part 1 is slightly worse than described. After WP_SECRETS_KEY is defined over stored secrets, the undecryptable test does report critical. But it says "There is no way to recover the original value. The credential must be re-entered". In fact, removing the constant recovers everything, and re-entering fails because writes are blocked too. PR 14847 already rewords that.
Review:
- PR 14848 (part 4) is ready.
- PR 14847 (parts 1 to 3) fixes what it describes. The tests fail on trunk and pass with the patch, and
--group secretspasses on single site and multisite. I've asked for one change: the new "key does not match the stored secrets" result namesWP_SECRETS_KEYand the salts for any keyring. With a keyring drop-in, such as a KMS that's down or one installed without re-wrapping the root key, it points at constants that didn't change and drops the keyring's error. The suggested change and a test are on the PR. Two smaller notes are there as well. One is the Info tab, which still describes the unused default keyring when a drop-in replaces the provider, the other half of part 3.
Once 14847 is updated, I'd like a committer to land both together for 7.2.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Follow-up to [64337]. Three problems in the new Secrets API Site Health tests, one commit each:
WP_Errorfromwp_list_secrets()into an empty result ("All secrets can be decrypted"), and the key source test matched "broken" in a translated string, so on translated sites a broken drop-in was reported as "using a dedicated key".secrets.phpthat replaces the provider (a host KMS or vault), the key source test described the unused default keyring and recommendedWP_SECRETS_KEY.WP_SECRETS_KEYwere told to define one "with a value fromwp secret generate-key", a command that is not in core or WP-CLI. Doing so after any secret is stored makes every secret unreadable and blocks new ones (the root key stays wrapped with the old site key;rotate_site_key()has no caller), while Site Health reported the new key as good. Now a key that cannot unwrap the root key is critical, a dedicated key is recommended only before secrets exist, and once they do, Site Health warns that changing the key or salts makes them unreadable.Testing
tests/phpunit/tests/secrets/siteHealth.php; all fail on trunk and pass with the patch.vendor/bin/phpunit --group secretspasses (393 tests).WP_SECRETS_KEYas the trunk advice says. Trunk: the secret becomes unreadable, writes fail, and the key source test says good. With the patch: "Secrets API key does not match the stored secrets" (critical). Removing the constant restores access in both cases.Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: finding these issues, drafting the fixes and tests, and running them. I reviewed every line and reproduced the manual steps above.
---
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
🤖 Generated with Claude Code