Fix unauthenticated rollback cancellation#633
Merged
Conversation
…ecret The nopriv boldgrid_cli_cancel_rollback AJAX endpoint was guarded only by a static backup_id (a CRC32 hash derived from site URL + admin email), allowing an unauthenticated attacker to cancel a pending auto-rollback by computing or brute-forcing the identifier. Fix generates a cryptographically random 32-character secret at restore-cron schedule time, stores it server-side, passes it as a CLI argument to the cron job, and requires it alongside backup_id on the cancel endpoint. The secret is deleted after a successful cancel so it cannot be replayed. Also upgrades PHPUnit from ^7 to ^9 and adds three new cron tests covering secret generation, rotation, and the no-archive early-exit path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the unauthenticated boldgrid_cli_cancel_rollback AJAX endpoint so pending auto-rollbacks cannot be canceled without an additional one-time secret, while keeping the endpoint usable by the CLI cron process.
Changes:
- Generate and store a cryptographically random one-time secret when scheduling a restore cron, and include it in the cron CLI args.
- Require the secret (in addition to
backup_id) on the nopriv cancel endpoint, usinghash_equals(), and delete the secret after a successful cancel. - Add/extend PHPUnit coverage for restore command secret generation/rotation and for cancel endpoint success/failure scenarios.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
admin/class-boldgrid-backup-admin-cron.php |
Generates and persists a one-time cancel secret and embeds it into the restore cron entry. |
admin/class-boldgrid-backup-admin-auto-rollback.php |
Enforces secret validation for CLI cancel AJAX and deletes the secret upon successful cancel. |
cli/class-site-restore.php |
Appends the new secret argument to the cancel URL (with URL encoding). |
tests/admin/test-class-boldgrid-backup-admin-cron.php |
Adds tests ensuring the restore command stores/rotates the secret and doesn’t store it when no archive exists. |
tests/admin/test-class-boldgrid-backup-admin-ajax.php |
Expands AJAX tests to require the secret and validate failure modes (missing/wrong secret). |
composer.json |
Updates dev dependency constraint for PHPUnit to ^9. |
composer.lock |
Updates locked dev dependency set (notably PHPUnit and related packages). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
PHPUnit 9.6.34 allows doctrine/instantiator ^1.5.0 || ^2. Composer was resolving to 2.1.0, which requires PHP ^8.4, breaking CI builds on PHP 7.4 and 8.0. Pinning to ^1.5 keeps the dependency compatible with both environments while PHPUnit 9 continues to work locally and in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The composer self-update --1 and composer remove/require cycle were designed to let Composer 1 pick the best phpunit version per PHP environment. Packagist shut down Composer 1 API support in September 2025, making composer require fail with "Could not find package". Now that composer.json constrains phpunit to ^9 with doctrine/instantiator pinned to ^1.5 (PHP 7.4/8.0 compatible) and composer.lock is committed, composer install -o is sufficient and correct for all CI environments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_arg_value() returns null when a CLI arg is absent. Passing null to rawurlencode() throws a TypeError on PHP 8+, which would fatal an already-scheduled pre-secret cron entry running after the plugin updates. Casting to string yields an empty string, letting the endpoint reject the request gracefully via its existing !empty() checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cssjoe
approved these changes
Mar 11, 2026
Member
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://imh-internal.atlassian.net/browse/ENG7-1452
Vulnerability
The boldgrid_cli_cancel_rollback nopriv AJAX endpoint was guarded only by a static backup_id. This identifier is a CRC32 hash
of the site URL and admin email:
hash( 'crc32', hash( 'sha512', site_url() . ' <' . $admin_email . '>' ) )
Because CRC32 produces only ~4 billion possible values, and the inputs are often discoverable (the site URL is public; the
admin email is frequently exposed via the WP REST API /wp-json/wp/v2/users/1 or author slugs), an unauthenticated attacker
could cancel a pending auto-rollback — silently preventing the safety net from reverting a broken plugin update.
The endpoint must remain nopriv because the CLI cron process has no WordPress session and cannot generate a nonce.
Replicating the vulnerability (before patch)
Prerequisites: BoldGrid Backup installed, auto-rollback enabled, at least one backup on disk.
job to restore the last backup in ~15 minutes.
- Reading wp_sitemeta where meta_key = 'boldgrid_backup_id'
- Computing hash('crc32', hash('sha512', 'https://example.com admin@example.com')) from the public site URL and
discoverable admin email
- Brute-forcing the ~4 billion CRC32 values
curl "https://example.com/wp-admin/admin-ajax.php?action=boldgrid_cli_cancel_rollback&backup_id=<backup_id>"
Fix
A cryptographically random 32-character secret is generated with wp_generate_password(32, false) each time a restore cron job
is scheduled. It is:
The CLI script already receives the secret in $argv and forwards it in the cancel URL automatically — no change in observable
behavior for legitimate usage. This mirrors the pattern already used by the boldgrid_backup_run_backup and
boldgrid_backup_run_restore nopriv endpoints.
Testing the patch
Run the automated test suite:
./vendor/bin/phpunit
Expected: 95 tests, 324 assertions, 0 failures.
Key tests covering this fix:
Manual verification (patched): Repeat the attack request without the secret:
curl "https://example.com/wp-admin/admin-ajax.php?action=boldgrid_cli_cancel_rollback&backup_id=<backup_id>"
Expected result: {"success":false,"data":"Invalid arguments"} — request is rejected. The legitimate cron-driven cancel flow
continues to work without any change.