Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion inc/class-sunrise.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ public static function load(): void {
$security_mode = (bool) (int) wu_get_setting_early('security_mode');

if ($security_mode) {
if (wu_get_isset($_GET, 'wu_secure') === wu_get_security_mode_key()) { // phpcs:ignore WordPress.Security.NonceVerification
$provided_key = wu_get_isset($_GET, 'wu_secure'); // phpcs:ignore WordPress.Security.NonceVerification

if (is_string($provided_key) && hash_equals(wu_get_security_mode_key(false), $provided_key)) {
wu_save_setting_early('security_mode', false);
} else {
/**
Expand Down
43 changes: 41 additions & 2 deletions inc/functions/sunrise.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,50 @@ function wu_save_setting_early($key, $value) {
}

/**
* Get the security mode key used to disable security mode
* Get the security mode key used to disable security mode.
*
* This key is exposed in an unauthenticated query string (?wu_secure=KEY) that
* turns the network-wide recovery "security mode" off, so it must be
* unpredictable. It used to be substr(md5(admin_email), 0, 6) — only ~24 bits
* and derived from a frequently public/guessable value, which an attacker could
* compute or brute-force. We now use a high-entropy random secret generated once
* and stored as a network option when the key is displayed to admins.
* random_bytes() is used (not wp_generate_password) because this runs from
* sunrise, before pluggable.php is loaded. Sunrise validation does not generate
* a new key while security mode is already active; if a random key has not been
* persisted yet, the legacy derived key remains valid until an admin loads the
* settings screen and sees the new random recovery URL.
*
* @since 2.0.20
*
* @param bool $generate Whether to generate and persist a random key when missing.
* @return string
*/
function wu_get_security_mode_key($generate = true): string {

$key = (string) get_network_option(null, 'wu_security_mode_key', '');

if ('' === $key) {
if (! $generate) {
return wu_get_legacy_security_mode_key();
}

$key = bin2hex(random_bytes(16));

update_network_option(null, 'wu_security_mode_key', $key);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

return $key;
}

/**
* Get the legacy security mode key used before high-entropy keys were persisted.
*
* @since 2.0.20
*
* @return string
*/
function wu_get_security_mode_key(): string {
function wu_get_legacy_security_mode_key(): string {

$hash = md5((string) get_network_option(null, 'admin_email'));

Expand Down
32 changes: 26 additions & 6 deletions tests/WP_Ultimo/Functions/Sunrise_Functions_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,37 +101,57 @@ public function test_save_setting_early_stores_value(): void {
}

/**
* Test wu_get_security_mode_key returns a 6-character string.
* Test wu_get_security_mode_key returns a high-entropy string.
*/
public function test_get_security_mode_key_returns_six_char_string(): void {
public function test_get_security_mode_key_returns_high_entropy_string(): void {

delete_network_option(null, 'wu_security_mode_key');

$key = wu_get_security_mode_key();

$this->assertIsString($key);
$this->assertSame(6, strlen($key));
$this->assertSame(32, strlen($key));
}

/**
* Test wu_get_security_mode_key returns only hex characters.
*/
public function test_get_security_mode_key_returns_hex_characters(): void {

delete_network_option(null, 'wu_security_mode_key');

$key = wu_get_security_mode_key();

$this->assertMatchesRegularExpression('/^[0-9a-f]{6}$/', $key);
$this->assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $key);
}

/**
* Test wu_get_security_mode_key is deterministic for same admin email.
* Test wu_get_security_mode_key is stable after generation.
*/
public function test_get_security_mode_key_is_deterministic(): void {
public function test_get_security_mode_key_is_stable_after_generation(): void {

delete_network_option(null, 'wu_security_mode_key');

$key1 = wu_get_security_mode_key();
$key2 = wu_get_security_mode_key();

$this->assertSame($key1, $key2);
}

/**
* Test wu_get_security_mode_key can preserve the legacy key without rotating.
*/
public function test_get_security_mode_key_without_generation_returns_legacy_key(): void {

delete_network_option(null, 'wu_security_mode_key');

$expected = substr(md5((string) get_network_option(null, 'admin_email')), 0, 6);
$key = wu_get_security_mode_key(false);

$this->assertSame($expected, $key);
$this->assertSame('', get_network_option(null, 'wu_security_mode_key', ''));
}

/**
* Test wu_kses_data returns string.
*/
Expand Down
Loading