Commit 6ddea0932f for wordpress.org
commit 6ddea0932fcb79afda7a6616c231f96140529570
Author: whyisjake <whyisjake@git.wordpress.org>
Date: Tue Nov 10 07:59:00 2026 +0000
Security: Harden secret names, parameters and the root key field.
Fixes three small issues in the Secrets API:
* `wp_secrets_validate_name()` accepted a name with a trailing newline, because `$` also matches before a final newline. The options API trims option names, so that name shared a row with the name without the newline, and a write through one made the other unreadable. The pattern now ends with `\z`.
* A secret value or key material could appear in a stack trace when an exception was thrown while a secret was being saved. The parameters that receive them are now marked with `#[\SensitiveParameter]`. Attributes are not inherited, so keyrings and providers in a drop-in need to add it themselves.
* Settings > All Settings showed `_wp_secrets_root_key` as an editable field, and saving the screen with it changed made every stored secret unreadable. The field is now masked, disabled and left out of the options the screen saves, like the connector API keys.
Follow-up to r64337.
Props dhrupo, aryanjasala, ericmann.
Fixes #66414. See #66187.
Built from https://develop.svn.wordpress.org/trunk@65375
git-svn-id: http://core.svn.wordpress.org/trunk@64514 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-admin/options.php b/wp-admin/options.php
index 8f065f6447..8b69bfe459 100644
--- a/wp-admin/options.php
+++ b/wp-admin/options.php
@@ -439,6 +439,10 @@ foreach ( (array) $options as $option ) :
// Mask connector API keys and prevent updates from this screen.
$value = _wp_connectors_mask_api_key( $option->option_value );
$disabled = true;
+ } elseif ( WP_Secrets_Key_Manager::ROOT_KEY_OPTION === $option->option_name ) {
+ // Replacing the root key makes every stored secret unreadable.
+ $value = _wp_connectors_mask_api_key( $option->option_value );
+ $disabled = true;
} else {
$value = $option->option_value;
$options_to_update[] = $option->option_name;
diff --git a/wp-includes/secrets.php b/wp-includes/secrets.php
index 826eb41b88..32b3a4b02e 100644
--- a/wp-includes/secrets.php
+++ b/wp-includes/secrets.php
@@ -253,7 +253,7 @@ function wp_secrets_validate_name( string $name ) {
);
}
- $segment_pattern = '/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/';
+ $segment_pattern = '/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?\z/';
if ( 0 === $slashes ) {
if ( ! preg_match( $segment_pattern, $name ) ) {
@@ -417,7 +417,11 @@ function wp_using_secrets_dropin(): bool {
* @param string $value The plaintext value to store.
* @return true|WP_Error
*/
-function wp_set_secret( string $name, $value ) {
+function wp_set_secret(
+ string $name,
+ #[\SensitiveParameter]
+ $value
+) {
return _wp_secrets_set( $name, $value, false );
}
@@ -544,7 +548,11 @@ function wp_list_secrets( string $namespace = '' ) { // phpcs:ignore Universal.N
* @param string $value The plaintext value to store.
* @return true|WP_Error
*/
-function wp_set_network_secret( string $name, $value ) {
+function wp_set_network_secret(
+ string $name,
+ #[\SensitiveParameter]
+ $value
+) {
return _wp_secrets_set( $name, $value, true );
}
@@ -711,7 +719,14 @@ function wp_secrets_provider_label(): string {
* wp_secret_changed.
* @return true|WP_Error
*/
-function _wp_secrets_set( string $name, $value, bool $network, bool $needs_rotation = false, ?string $action_override = null ) {
+function _wp_secrets_set(
+ string $name,
+ #[\SensitiveParameter]
+ $value,
+ bool $network,
+ bool $needs_rotation = false,
+ ?string $action_override = null
+) {
return _wp_secrets_get_provider()->set( $name, $value, $network, $needs_rotation, $action_override );
}
diff --git a/wp-includes/secrets/class-wp-secrets-broken-keyring.php b/wp-includes/secrets/class-wp-secrets-broken-keyring.php
index 85b51f9e05..1ad873345a 100644
--- a/wp-includes/secrets/class-wp-secrets-broken-keyring.php
+++ b/wp-includes/secrets/class-wp-secrets-broken-keyring.php
@@ -30,7 +30,10 @@ final class WP_Secrets_Broken_Keyring implements WP_Secrets_Keyring {
* @param string $key_material Ignored.
* @return WP_Error
*/
- public function wrap( string $key_material ): WP_Error {
+ public function wrap(
+ #[\SensitiveParameter]
+ string $key_material
+ ): WP_Error {
return $this->error();
}
diff --git a/wp-includes/secrets/class-wp-secrets-broken-provider.php b/wp-includes/secrets/class-wp-secrets-broken-provider.php
index 8c429c55e0..8edeee93b1 100644
--- a/wp-includes/secrets/class-wp-secrets-broken-provider.php
+++ b/wp-includes/secrets/class-wp-secrets-broken-provider.php
@@ -66,7 +66,14 @@ final class WP_Secrets_Broken_Provider implements WP_Secrets_Provider {
* @param string|null $action Ignored.
* @return WP_Error
*/
- public function set( string $name, $value, bool $network = false, bool $needs_rotation = false, ?string $action = null ): WP_Error {
+ public function set(
+ string $name,
+ #[\SensitiveParameter]
+ $value,
+ bool $network = false,
+ bool $needs_rotation = false,
+ ?string $action = null
+ ): WP_Error {
return $this->error();
}
diff --git a/wp-includes/secrets/class-wp-secrets-cipher.php b/wp-includes/secrets/class-wp-secrets-cipher.php
index 5cb12848d3..4880d02317 100644
--- a/wp-includes/secrets/class-wp-secrets-cipher.php
+++ b/wp-includes/secrets/class-wp-secrets-cipher.php
@@ -69,7 +69,16 @@ final class WP_Secrets_Cipher {
* @phpstan-param 'site'|'network' $scope
* @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $slot
*/
- public function encrypt_value( string $master_key, string $scope, int $site_id, string $name, string $slot, $plaintext ) {
+ public function encrypt_value(
+ #[\SensitiveParameter]
+ string $master_key,
+ string $scope,
+ int $site_id,
+ string $name,
+ string $slot,
+ #[\SensitiveParameter]
+ $plaintext
+ ) {
$check = $this->validate_common( $master_key, $scope, $site_id, $name, $slot );
if ( is_wp_error( $check ) ) {
@@ -142,7 +151,15 @@ final class WP_Secrets_Cipher {
* @phpstan-param 'site'|'network' $scope
* @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $slot
*/
- public function decrypt_value( string $master_key, string $scope, int $site_id, string $name, string $slot, $record ) {
+ public function decrypt_value(
+ #[\SensitiveParameter]
+ string $master_key,
+ string $scope,
+ int $site_id,
+ string $name,
+ string $slot,
+ $record
+ ) {
$check = $this->validate_common( $master_key, $scope, $site_id, $name, $slot );
if ( is_wp_error( $check ) ) {
@@ -211,7 +228,12 @@ final class WP_Secrets_Cipher {
* @param string $plaintext Value to fingerprint.
* @return string|WP_Error 32-character hex string on success. WP_Error on failure.
*/
- public function fingerprint( string $master_key, $plaintext ) {
+ public function fingerprint(
+ #[\SensitiveParameter]
+ string $master_key,
+ #[\SensitiveParameter]
+ $plaintext
+ ) {
if ( ! $this->is_valid_master_key( $master_key ) ) {
return new WP_Error(
WP_SECRETS_ERROR_KEY_UNAVAILABLE,
diff --git a/wp-includes/secrets/class-wp-secrets-config-key-provider.php b/wp-includes/secrets/class-wp-secrets-config-key-provider.php
index c99adc925e..819814f488 100644
--- a/wp-includes/secrets/class-wp-secrets-config-key-provider.php
+++ b/wp-includes/secrets/class-wp-secrets-config-key-provider.php
@@ -84,7 +84,10 @@ final class WP_Secrets_Config_Key_Provider implements WP_Secrets_Keyring {
* @param string $key_material Raw key material to protect.
* @return string|WP_Error
*/
- public function wrap( string $key_material ) {
+ public function wrap(
+ #[\SensitiveParameter]
+ string $key_material
+ ) {
if ( '' === $key_material ) {
return new WP_Error(
WP_SECRETS_ERROR_INVALID_VALUE,
diff --git a/wp-includes/secrets/class-wp-secrets-libsodium-provider.php b/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
index e19fa03aff..3481e28f46 100644
--- a/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
+++ b/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
@@ -245,7 +245,15 @@ final class WP_Secrets_Libsodium_Provider implements WP_Secrets_Provider {
* @phpstan-return array{dk: string, dk_nonce: string, ct: string, nonce: string, fingerprint: string, created: int, needs_rotation: bool}|WP_Error
* @phpstan-param 'site'|'network' $scope
*/
- private function demote_slot( WP_Secrets_Cipher $cipher, string $master_key, string $scope, int $site_id, string $name, array $current_slot ) {
+ private function demote_slot(
+ WP_Secrets_Cipher $cipher,
+ #[\SensitiveParameter]
+ string $master_key,
+ string $scope,
+ int $site_id,
+ string $name,
+ array $current_slot
+ ) {
$plaintext = $cipher->decrypt_value( $master_key, $scope, $site_id, $name, WP_Secret_Version::CURRENT, $current_slot );
if ( is_wp_error( $plaintext ) ) {
@@ -289,7 +297,14 @@ final class WP_Secrets_Libsodium_Provider implements WP_Secrets_Provider {
* wp_import_option_as_secret() passes 'imported'.
* @return true|WP_Error
*/
- public function set( string $name, $value, bool $network = false, bool $needs_rotation = false, ?string $action = null ) {
+ public function set(
+ string $name,
+ #[\SensitiveParameter]
+ $value,
+ bool $network = false,
+ bool $needs_rotation = false,
+ ?string $action = null
+ ) {
$name_check = wp_secrets_validate_name( $name );
if ( is_wp_error( $name_check ) ) {
diff --git a/wp-includes/secrets/interface-wp-secrets-keyring.php b/wp-includes/secrets/interface-wp-secrets-keyring.php
index d378062908..044e8270be 100644
--- a/wp-includes/secrets/interface-wp-secrets-keyring.php
+++ b/wp-includes/secrets/interface-wp-secrets-keyring.php
@@ -30,12 +30,18 @@ interface WP_Secrets_Keyring {
* re-wrapped value with update_site_option(), which reports an unchanged value
* as a failure, and WP_Secrets_Keyring_Conformance checks this.
*
+ * Attributes are not inherited, so an implementation must mark $key_material
+ * with #[\SensitiveParameter] itself to keep it out of stack traces.
+ *
* @since 7.2.0
*
* @param string $key_material Raw key material to protect.
* @return string|WP_Error Opaque wrapped value on success, WP_Error on failure.
*/
- public function wrap( string $key_material );
+ public function wrap(
+ #[\SensitiveParameter]
+ string $key_material
+ );
/**
* Unwraps (decrypts) previously wrapped key material.
diff --git a/wp-includes/secrets/interface-wp-secrets-provider.php b/wp-includes/secrets/interface-wp-secrets-provider.php
index d1916aa723..671855afa5 100644
--- a/wp-includes/secrets/interface-wp-secrets-provider.php
+++ b/wp-includes/secrets/interface-wp-secrets-provider.php
@@ -138,7 +138,14 @@ interface WP_Secrets_Provider {
* 'updated'.
* @return true|WP_Error
*/
- public function set( string $name, $value, bool $network = false, bool $needs_rotation = false, ?string $action = null );
+ public function set(
+ string $name,
+ #[\SensitiveParameter]
+ $value,
+ bool $network = false,
+ bool $needs_rotation = false,
+ ?string $action = null
+ );
/**
* Deletes a secret.
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 1b29f910c8..23b15c16ca 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta4-65374';
+$wp_version = '7.2-beta4-65375';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.