Commit e9cf430389 for wordpress.org

commit e9cf430389458557819ffeb8eee4b4d8c8ccf733
Author: whyisjake <whyisjake@git.wordpress.org>
Date:   Tue Nov 10 15:43:12 2026 +0000

    Security: Generate `WP_SECRETS_KEY` during installation.

    Since r65385 the Secrets API has no key to fall back on, so a new site had no working Secrets API until someone added `WP_SECRETS_KEY` to `wp-config.php` by hand.

    `wp-config-sample.php` now defines `WP_SECRETS_KEY` with the placeholder, after the keys and salts. It sits outside their group because, unlike them, this key must not change once secrets are stored. The Secrets API rejects the placeholder, so a sample that is copied by hand and left unedited does not become a usable key.

    `setup-config.php` fills the key in when it fills in the salts. The value is a base64-encoded random 32-byte key. Without a source of random bytes it comes from `wp_generate_password()`, as the salts do. When the sample has no `WP_SECRETS_KEY` line, as in a custom or older localized sample, the define is added after the salts.

    Existing sites are not affected. Tools that write `wp-config.php` themselves, such as WP-CLI's `wp config create`, need the same change separately.

    Follow-up to r64337, r65385.

    Props ericmann, peterwilsoncc, digitalpritam, ravipatel, Clorith, dhrupo.
    Fixes #66399. See #66187.

    Built from https://develop.svn.wordpress.org/trunk@65389


    git-svn-id: http://core.svn.wordpress.org/trunk@64528 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-admin/setup-config.php b/wp-admin/setup-config.php
index aaa91fc24b..67561c7f41 100644
--- a/wp-admin/setup-config.php
+++ b/wp-admin/setup-config.php
@@ -390,13 +390,29 @@ switch ( $step ) {
 			}
 		}

-		$key = 0;
+		/*
+		 * The Secrets API key is generated like the salts above, but written as base64-encoded
+		 * random bytes, the form the Secrets API uses as raw key material.
+		 */
+		try {
+			$secrets_key = base64_encode( random_bytes( 32 ) );
+		} catch ( Exception $ex ) {
+			$secrets_key = wp_generate_password( 64, true, true );
+		}
+
+		$key             = 0;
+		$has_secrets_key = false;
 		foreach ( $config_file as $line_num => $line ) {
 			if ( str_starts_with( $line, '$table_prefix =' ) ) {
 				$config_file[ $line_num ] = '$table_prefix = \'' . addcslashes( $prefix, "\\'" ) . "';\r\n";
 				continue;
 			}

+			if ( isset( $nonce_salt_line ) && ! isset( $salts_end_line ) && '/**#@-*/' === trim( $line ) ) {
+				$salts_end_line = $line_num;
+				continue;
+			}
+
 			if ( ! preg_match( '/^define\(\s*\'([A-Z_]+)\',([ ]+)/', $line, $match ) ) {
 				continue;
 			}
@@ -426,10 +442,36 @@ switch ( $step ) {
 				case 'NONCE_SALT':
 					$config_file[ $line_num ] = "define( '" . $constant . "'," . $padding . "'" . $secret_keys[ $key++ ] . "' );\r\n";
 					break;
+				case 'WP_SECRETS_KEY':
+					$config_file[ $line_num ] = "define( '" . $constant . "'," . $padding . "'" . $secrets_key . "' );\r\n";
+					$has_secrets_key          = true;
+					break;
+			}
+
+			if ( 'NONCE_SALT' === $constant ) {
+				$nonce_salt_line = $line_num;
 			}
 		}
 		unset( $line );

+		// A custom or older localized wp-config-sample.php may not define the key: add it after the salts.
+		if ( ! $has_secrets_key && isset( $nonce_salt_line ) ) {
+			$secrets_key_lines = array(
+				"\r\n",
+				"/**\r\n",
+				" * Encryption key for the Secrets API.\r\n",
+				" *\r\n",
+				" * Secrets stored by WordPress and plugins are encrypted with this key.\r\n",
+				" * Unlike the keys and salts above, changing it makes every stored secret unreadable.\r\n",
+				" *\r\n",
+				" * @since 7.2.0\r\n",
+				" */\r\n",
+				"define( 'WP_SECRETS_KEY', '" . $secrets_key . "' );\r\n",
+			);
+
+			array_splice( $config_file, ( $salts_end_line ?? $nonce_salt_line ) + 1, 0, $secrets_key_lines );
+		}
+
 		if ( ! is_writable( ABSPATH ) ) :
 			setup_config_display_header();
 			?>
diff --git a/wp-config-sample.php b/wp-config-sample.php
index 8f96fb0f4c..1fd29f56b6 100644
--- a/wp-config-sample.php
+++ b/wp-config-sample.php
@@ -59,6 +59,17 @@ define( 'NONCE_SALT',       'put your unique phrase here' );

 /**#@-*/

+/**
+ * Encryption key for the Secrets API.
+ *
+ * Secrets stored by WordPress and plugins are encrypted with this key. Change it to
+ * a unique phrase, different from the keys and salts above. Unlike them, it must not
+ * be changed once secrets are stored: that makes every stored secret unreadable.
+ *
+ * @since 7.2.0
+ */
+define( 'WP_SECRETS_KEY', 'put your unique phrase here' );
+
 /**
  * WordPress database table prefix.
  *
diff --git a/wp-includes/version.php b/wp-includes/version.php
index a65064fbed..4a5e36c4cc 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta4-65388';
+$wp_version = '7.2-beta4-65389';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.