Skip to content

Security: use a high-entropy random key for the security-mode disable URL#1382

Open
vuckro wants to merge 3 commits into
Ultimate-Multisite:mainfrom
vuckro:security/sunrise-security-mode-key
Open

Security: use a high-entropy random key for the security-mode disable URL#1382
vuckro wants to merge 3 commits into
Ultimate-Multisite:mainfrom
vuckro:security/sunrise-security-mode-key

Conversation

@vuckro

@vuckro vuckro commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

The unauthenticated ?wu_secure=KEY query string that turns the network-wide
recovery security mode off used substr(md5(admin_email), 0, 6) as the key
— only ~24 bits and derived from a commonly public/guessable value. An attacker
could compute or brute-force it and remotely disable the admin's safe-mode
lockdown (re-enabling all other plugins).

Changes

  • Generate a 128-bit random key once (random_bytes — this runs from sunrise,
    before pluggable.php) and store it as a network option.
  • Compare it with hash_equals().

The key is already displayed on the Settings screen ("copy this URL to disable
security mode"), so the documented recovery workflow is unaffected — the
settings note now shows the new random URL.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened security-mode checks to prevent bypasses and timing attacks.
    • Recovery key now uses a high-entropy stored secret for more robust protection.
    • Maintains legacy compatibility when explicit recovery-key generation is disabled.

The unauthenticated ?wu_secure=KEY query string that turns the network-wide
recovery "security mode" off used substr(md5(admin_email), 0, 6) as the key —
only ~24 bits and derived from a commonly public/guessable value, so an attacker
could compute or brute-force it and remotely disable the admin's safe-mode
lockdown.

Generate a 128-bit random key once (random_bytes, since this runs from sunrise
before pluggable.php) and store it as a network option, and compare it with
hash_equals(). The key is already displayed on the settings screen, so the
documented "copy this URL to disable security mode" workflow is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65309bf7-c877-4107-a190-f8fcde9c1c02

📥 Commits

Reviewing files that changed from the base of the PR and between 708d48c and 287c015.

📒 Files selected for processing (3)
  • inc/class-sunrise.php
  • inc/functions/sunrise.php
  • tests/WP_Ultimo/Functions/Sunrise_Functions_Test.php

📝 Walkthrough

Walkthrough

Security-mode authentication now uses a persisted 16-byte random hex secret (with a legacy fallback) and validates incoming wu_secure query values using a timing-safe hash_equals() check.

Changes

Security mode authentication hardening

Layer / File(s) Summary
Secure random key generation with persistence
inc/functions/sunrise.php, tests/WP_Ultimo/Functions/Sunrise_Functions_Test.php
wu_get_security_mode_key() docblock and implementation now persist a 16-byte random hex secret to network option wu_security_mode_key on first generation, return the persisted value thereafter, and expose wu_get_legacy_security_mode_key() for no-generation calls; tests updated to expect 32-hex-character keys, stability after generation, and legacy behavior when generation is disabled.
Timing-safe comparison with hash_equals
inc/class-sunrise.php
Sunrise::load() extracts wu_secure into $provided_key, ensures it is a string, and compares it to wu_get_security_mode_key(false) using hash_equals() instead of direct equality.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble secrets, deep and neat,
Sixteen bytes wrapped up in heat,
Saved in options, quiet and snug,
Compared in time-safe, gentle tug,
A rabbit cheers — secure and sweet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the primary security change: implementing a high-entropy random key to replace the weak deterministic key for the security-mode disable URL.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/class-sunrise.php`:
- Line 338: The code currently casts wu_get_isset($_GET, 'wu_secure') to string
and passes it to hash_equals, which can trigger "Array to string conversion" for
inputs like ?wu_secure[]=x; update the check in the sunrise bootstrap (around
the hash_equals call) to first retrieve the value via wu_get_isset($_GET,
'wu_secure'), verify it is a string (e.g. is_string(...) or ensure it's scalar
and specifically a string), and only then call
hash_equals(wu_get_security_mode_key(), $value). If the value is not a string,
skip the hash_equals branch (treat as non-matching) to avoid warnings and
potential log spam. Ensure you reference the wu_get_isset call, the 'wu_secure'
GET key, and the hash_equals/wu_get_security_mode_key invocation when
implementing the guard.

In `@inc/functions/sunrise.php`:
- Around line 91-96: The code in Sunrise::load() currently auto-generates and
persists a new wu_security_mode_key on first read (using get_network_option,
random_bytes, bin2hex, update_network_option), which silently invalidates
pre-issued recovery URLs; instead, stop creating a new random key during
ordinary reads — if wu_security_mode_key is missing, temporarily accept the
legacy/derived key (compute the legacy value the same way the Settings screen
previously derived it) until an admin explicitly sets or a migration persists a
new random key; move the update_network_option(random key) call out of
Sunrise::load() into an explicit migration/upgrade routine or the settings save
path so the secret is only rotated when persisted intentionally, and ensure
load() checks both the persisted option and the legacy-derived key for
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5778e68c-3587-413b-b8e2-f685b86e88c3

📥 Commits

Reviewing files that changed from the base of the PR and between 1310078 and 708d48c.

📒 Files selected for processing (2)
  • inc/class-sunrise.php
  • inc/functions/sunrise.php

Comment thread inc/class-sunrise.php Outdated
Comment thread inc/functions/sunrise.php
@superdav42

Copy link
Copy Markdown
Collaborator

CLAIM_RELEASED reason=worker_complete runner=superdav42 ts=2026-06-10T13:55:22Z aidevops_version=3.20.46 opencode_version=1.16.2

@superdav42 superdav42 added the status:available Task is available for claiming label Jun 10, 2026
@superdav42

Copy link
Copy Markdown
Collaborator

Permission check failed for this PR (HTTP unknown from collaborator permission API). Unable to determine if @vuckro is a maintainer or external contributor. A maintainer must review and merge this PR manually. This is a fail-closed safety measure — the pulse will not auto-merge until the permission API succeeds.


aidevops.sh v3.20.52 plugin for OpenCode v1.17.3 with gpt-5.5 spent 2m and 47,696 tokens on this as a headless worker.

@superdav42

Copy link
Copy Markdown
Collaborator

Implemented a compatible follow-up branch/PR for the failing checks: #1413.

Evidence:

  • PR Security: use a high-entropy random key for the security-mode disable URL #1382 failed in E2E service startup while pulling axllent/mailpit:latest from Docker Hub, then cleanup failed because checkout never completed and tests/e2e/cypress / package.json were absent.
  • ci: fix PR #1382 E2E cleanup failure #1413 carries forward the security-mode changes from this PR and adds the workflow-only CI fix:
    • .github/workflows/e2e.yml: use ghcr.io/axllent/mailpit:latest.
    • .github/workflows/e2e.yml: guard Cypress permissions cleanup when tests/e2e/cypress is absent.
    • .github/workflows/e2e.yml: guard npm run env:stop when package.json is absent.

Verification run locally on the follow-up branch:

  • git diff --check
  • php -l inc/functions/sunrise.php && php -l inc/class-sunrise.php && php -l tests/WP_Ultimo/Functions/Sunrise_Functions_Test.php
  • vendor/bin/phpcs inc/functions/sunrise.php inc/class-sunrise.php tests/WP_Ultimo/Functions/Sunrise_Functions_Test.php
  • Parsed .github/workflows/e2e.yml with PyYAML successfully.

Blocked from updating this fork PR branch directly, so #1413 is the upstream-compatible replacement/follow-up for the same change set plus the CI failure fix.


superdav42 added a commit that referenced this pull request Jun 12, 2026
* fix(security): use a high-entropy random key for security-mode disable

The unauthenticated ?wu_secure=KEY query string that turns the network-wide
recovery "security mode" off used substr(md5(admin_email), 0, 6) as the key —
only ~24 bits and derived from a commonly public/guessable value, so an attacker
could compute or brute-force it and remotely disable the admin's safe-mode
lockdown.

Generate a 128-bit random key once (random_bytes, since this runs from sunrise
before pluggable.php) and store it as a network option, and compare it with
hash_equals(). The key is already displayed on the settings screen, so the
documented "copy this URL to disable security mode" workflow is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): preserve legacy security mode recovery URL

* ci: guard e2e cleanup before checkout

* fix: address security mode review feedback

---------

Co-authored-by: vuckro <maribel_waters@howtocore.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@superdav42

Copy link
Copy Markdown
Collaborator

Rebase needed — PR has merge conflicts and no origin:* label

This PR has merge conflicts against the default branch and lacks both origin:interactive and origin:worker labels. The pulse merge pass treats it as the label-agnostic stuck state (t3193) and surfaces it here with a one-shot rebase nudge.

To resolve

git fetch origin
git checkout security/sunrise-security-mode-key
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

Or use the GitHub web UI's Update branch button if the conflicts are trivial enough.

Why this PR slipped through the existing nudges

The existing rebase-nudge family (_post_rebase_nudge_on_interactive_conflicting, _post_rebase_nudge_on_contributor_conflicting, _post_rebase_nudge_on_worker_conflicting) keys on the origin:* labels. PRs created without those labels — typically docs/migration commits made directly via the web UI or via a worker that didn't apply its origin label — were silently re-evaluated every cycle without surfacing.

Posted automatically by pulse-merge-stuck.sh (t3193 / GH#21895).


aidevops.sh v3.20.57 automated scan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor status:available Task is available for claiming

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants