Skip to content
Merged
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
16 changes: 13 additions & 3 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

services:
mailpit:
image: axllent/mailpit:latest
image: ghcr.io/axllent/mailpit@sha256:3bd7c2f2696deb35a4780d152b404dceec99cb041b942c0877b3b22384714f85
ports:
- 1025:1025
- 8025:8025
Expand Down Expand Up @@ -520,7 +520,12 @@ jobs:

- name: Fix permissions for Cypress output
if: always()
run: sudo chown -R $USER:$USER tests/e2e/cypress
run: |
if [ -d tests/e2e/cypress ]; then
sudo chown -R $USER:$USER tests/e2e/cypress
else
echo "Cypress output directory does not exist; skipping permission fix."
fi

- name: Upload Cypress screenshots
if: always()
Expand All @@ -540,4 +545,9 @@ jobs:

- name: Stop WordPress Environment
if: always()
run: npm run env:stop
run: |
if [ -f package.json ]; then
npm run env:stop
else
echo "package.json does not exist; checkout did not complete before cleanup."
fi
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
49 changes: 47 additions & 2 deletions inc/functions/sunrise.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,56 @@ 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();
}

$generated_key = bin2hex(random_bytes(16));

add_network_option(null, 'wu_security_mode_key', $generated_key);

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

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

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
47 changes: 41 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,72 @@ 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 without generation returns a stored key.
*/
public function test_get_security_mode_key_without_generation_returns_persisted_key_when_available(): void {

delete_network_option(null, 'wu_security_mode_key');

$generated = wu_get_security_mode_key();
$stored = get_network_option(null, 'wu_security_mode_key', '');

$this->assertSame($generated, $stored);
$this->assertSame($stored, wu_get_security_mode_key(false));
$this->assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $stored);
}

/**
* 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', ''));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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