Skip to content

Security: Harden secret names, value parameters and the root key on All Settings. - #15200

Open
dhrupo wants to merge 1 commit into
WordPress:trunkfrom
dhrupo:66414-secrets-hardening
Open

dhrupo wants to merge 1 commit into
WordPress:trunkfrom
dhrupo:66414-secrets-hardening

Conversation

@dhrupo

@dhrupo dhrupo commented Oct 30, 2026 •

Copy link
Copy Markdown

Fixes the three Secrets API issues reported on the ticket.

  1. Trailing newline in a name. wp_secrets_validate_name() used $, which also matches before a final newline, so "test/key\n" was valid. Option names are trimmed, so it shared a row with test/key while encrypting for its own name, and a write through one name broke the other. The pattern now ends with \z.
  2. Values and keys in stack traces. The plaintext parameters on the wp_set_secret() / wp_set_network_secret() path are marked #[\SensitiveParameter]: both functions, _wp_secrets_set(), WP_Secrets_Provider::set() and its two implementations, and WP_Secrets_Cipher::encrypt_value() / fingerprint(). So are the parameters that carry key material: $key_material on WP_Secrets_Keyring::wrap() and its two implementations, and $master_key on WP_Secrets_Cipher::encrypt_value(), decrypt_value() and fingerprint() and WP_Secrets_Libsodium_Provider::demote_slot(). Attributes are not inherited, so the wrap() docblock tells drop-in keyrings to add it themselves. On PHP < 8.2 the attribute line is a comment.
  3. Root key on Settings > All Settings. _wp_secrets_root_key is now masked, disabled and left out of page_options, like the connector API keys. Before, one save with the field changed replaced the key and every stored secret became unreadable.

Testing

New tests: --group 66414, plus three rows in Tests_Secrets_WpSecretsValidateName::data_invalid_names.

  • trunk, PHP 8.4: 20 failures. The trace and attribute tests require PHP 8.2 and skip below it.
  • this branch: OK (17 tests, 22 assertions) and OK (3 tests, 6 assertions) on PHP 8.2 and 8.4. On PHP 7.4 the name rows pass (OK (3 tests, 6 assertions)) and the PHP 8.2 tests skip.
  • --group secrets: 407 tests, no failures, on PHP 7.4, 8.2 and 8.4 (single site) and PHP 8.4 (multisite).

Browser check on Settings > All Settings (Playwright, Chromium):

  • trunk: the root key field is editable and shows the stored value. A forced save stored replaced-by-a-mistaken-save as the root key.
  • this branch: the field is disabled and masked, it is not in page_options, and a forced save leaves the key and an existing secret unchanged.

Screenshots: before, after.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Writing the tests and the patch, and running the checks above. I reviewed every line and checked the results myself.


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.

@github-actions

github-actions Bot commented Oct 30, 2026 •

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.

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

Props dhrupo, ericmann.

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@ericmann ericmann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dhrupo. All three fixes look right:

  • \z: both preg_match() calls in wp_secrets_validate_name() use $segment_pattern, so this covers namespaced and unnamespaced names.
  • All Settings: _wp_secrets_root_key is the only Secrets API row that renders as editable there. Secret records are arrays, so they already show as serialized data, and network rows live in wp_sitemeta, which the screen doesn't list.
  • #[\SensitiveParameter]: on its own line, the attribute is a comment before PHP 8.2, the same as in pluggable.php.

One suggestion. Key material can reach a stack trace too:

  • WP_Secrets_Keyring::wrap( $key_material ) receives the raw root key. A drop-in keyring that calls a KMS client can throw from inside it.
  • The $master_key parameters of WP_Secrets_Cipher::encrypt_value(), decrypt_value() and fingerprint() carry the derived key.

Could you mark those too? That means wrap() in WP_Secrets_Config_Key_Provider, WP_Secrets_Broken_Keyring and the interface. On the interface it's docs only, because attributes aren't inherited, so a drop-in keyring needs its own. One sentence in the interface docblock would tell drop-in authors that.

…ll Settings.

Three small fixes to the Secrets API:

* `wp_secrets_validate_name()` anchors its segment pattern with `\z`. With `$`, a name with a trailing newline passed, and since option names are trimmed it shared a row with the name without the newline while encrypting for itself.
* Parameters that carry a plaintext value through `wp_set_secret()` and `wp_set_network_secret()` are marked `#[\SensitiveParameter]`, and so are the parameters that carry key material: `$key_material` on `WP_Secrets_Keyring::wrap()` and its implementations, and `$master_key` on the `WP_Secrets_Cipher` methods and `WP_Secrets_Libsodium_Provider::demote_slot()`. An exception thrown while saving does not print the value or a key in a stack trace.
* Settings > All Settings masks `_wp_secrets_root_key` and excludes it from the save, as it already does for connector API keys. Replacing the root key made every stored secret unreadable.

Props aryanjasala, ericmann, dhrupo.
Fixes #66414.
@dhrupo
dhrupo force-pushed the 66414-secrets-hardening branch from f891686 to db24fa7 Compare October 30, 2026 18:55
@dhrupo

dhrupo commented Oct 30, 2026

Copy link
Copy Markdown
Author

Thanks @ericmann. Done in db24fa7: $key_material on wrap() in WP_Secrets_Keyring, WP_Secrets_Config_Key_Provider and WP_Secrets_Broken_Keyring, and $master_key on WP_Secrets_Cipher::encrypt_value(), decrypt_value() and fingerprint(). The interface's wrap() docblock now says implementations must add the attribute themselves, since it isn't inherited.

I also marked $master_key on the private WP_Secrets_Libsodium_Provider::demote_slot(). It passes the key to encrypt_value(), so an exception from random_bytes() there would otherwise still show the key in the frame above. test_parameters_that_receive_a_secret_value_are_sensitive covers all seven.

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.

2 participants