# REPRO-2026-00233: Grav CMS unsafe deserialization and command injection RCE ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00233 CVE: CVE-2026-56700 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause ## Summary Grav CMS before 2.0.0-beta.2 contains an unsafe deserialization vulnerability in the modern scheduler job queue. A queue item may contain a `serialized_job` field, and vulnerable versions reconstruct that field with `unserialize(base64_decode(...))` without integrity protection. A forged queue item containing a serialized `Grav\Common\Scheduler\Job` can therefore be processed by the real scheduler and execute an attacker-selected command when the scheduler is triggered through the real HTTP scheduler-webhook endpoint. ## Impact - Package/component affected: `getgrav/grav`, specifically `Grav\Common\Scheduler\JobQueue` and the scheduler execution path. - Affected versions: Grav CMS versions before 2.0.0-beta.2. The reproduction uses 2.0.0-beta.1 as the vulnerable version and 2.0.0-beta.2 as the fixed negative control. - Risk level and consequences: critical. If an attacker can place or tamper with a scheduler queue item and trigger scheduler processing, the queue deserialization path can instantiate a forged scheduler `Job` and execute arbitrary commands in the PHP/Grav process context. ## Impact Parity - Disclosed/claimed maximum impact: remote/API-triggered arbitrary code execution through unsafe deserialization in Grav components, including `Scheduler\JobQueue`. - Reproduced impact from this run: code execution. The proof crafts real serialized `Grav\Common\Scheduler\Job` payloads whose command writes marker files, starts a real Grav web server with the real scheduler-webhook plugin, and sends `POST /scheduler/webhook` to trigger `Scheduler::processWebhookTrigger()` -> `Scheduler::run()` -> `JobQueue::popWithId()` / `reconstructJob()`. - Parity: `full`. - Not demonstrated: the reproduction focuses on the `Scheduler\JobQueue` vector. It does not separately exploit the `FileCache` or `Session::getFlashObject` unsafe unserialize sinks. ## Root Cause The vulnerable `JobQueue::reconstructJob()` path trusted the on-disk queue item field `serialized_job` and directly performed: ```php $job = unserialize(base64_decode((string) $item['serialized_job'])); ``` No HMAC, signature, or class restriction prevented an attacker-planted/tampered queue item from supplying a serialized `Grav\Common\Scheduler\Job`. The `Job` object has an execution path that invokes its configured command/arguments, so a forged serialized job with command `system` and attacker-controlled arguments becomes a direct PHP object injection to command execution primitive. The fixed version signs the serialized job blob with an HMAC derived from Grav's nonce key and only unserializes when the sibling `serialized_job_hmac` verifies. Missing or mismatched HMAC values fall through to the structured queue fields instead of trusting the serialized object. The advisory identifies the fix as Grav core commit `c66dfeb5f` for JobQueue/FileCache/Session/InstallCommand hardening, released in 2.0.0-beta.2. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script reuses/clones the real Grav repository, checks out 2.0.0-beta.1 and 2.0.0-beta.2, installs Composer dependencies, installs/enables the real `scheduler-webhook` plugin, starts the real PHP/Grav web server, poisons the real scheduler queue with a forged `serialized_job`, and sends `POST /scheduler/webhook` to trigger scheduler processing over HTTP. 3. Expected evidence: vulnerable 2.0.0-beta.1 creates `bundle/repro/proof_vulnerable_1.txt` and `bundle/repro/proof_vulnerable_2.txt`; fixed 2.0.0-beta.2 does not create fixed proof files and logs execution of the benign `/bin/true` fallback job instead. ## Evidence - Runtime manifest: `bundle/repro/runtime_manifest.json` records `entrypoint_kind="api_remote"`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`. - Structured verdict: `bundle/repro/validation_verdict.json` records `claim_outcome="confirmed"`, `validated_surface="api_remote"`, and `observed_impact_class="code_execution"`. - Vulnerable proof files: - `bundle/repro/proof_vulnerable_1.txt` contains `GRAV_JOBQUEUE_WEBHOOK_RCE_vulnerable_1`. - `bundle/repro/proof_vulnerable_2.txt` contains `GRAV_JOBQUEUE_WEBHOOK_RCE_vulnerable_2`. - Vulnerable HTTP evidence: - `bundle/logs/vulnerable_webhook_response_1_body.json` shows the webhook returned success and `jobs_run: 1`. - `bundle/logs/vulnerable_scheduler_1.log` and `bundle/logs/vulnerable_scheduler_2.log` show the forged job completed successfully with command `system`. - Fixed negative control: - `bundle/logs/fixed_scheduler_1.log` and `bundle/logs/fixed_scheduler_2.log` show the fixed version completed the benign fallback job (`/bin/true`) rather than the attacker-controlled serialized `Job`. - No `proof_fixed_*.txt` files are produced. - Patch-gap evidence: - `bundle/logs/vulnerable_jobqueue_gap.log` contains the vulnerable unsigned `unserialize(base64_decode(...))` sink. - `bundle/logs/fixed_jobqueue_gap.log` contains the HMAC validation logic added in the fixed version. - Environment details: - Grav vulnerable tag: 2.0.0-beta.1 (`26a2d519c59c620e2b0a54d0baf33889d7d5db0a`). - Grav fixed tag: 2.0.0-beta.2 (`f95b0ff51a655edcbcc060a3d74b43e3f20b9585`). - Scheduler-webhook plugin commit used for the HTTP endpoint: recorded in `bundle/logs/webhook_plugin_commit.log`. ## Recommendations / Next Steps - Upgrade Grav CMS to 2.0.0-beta.2 or later. - Preserve HMAC/integrity validation for every serialized queue/cache/session payload and reject or safely rebuild unsigned legacy data rather than unserializing it. - Prefer avoiding PHP object serialization for attacker-influenced persistent data. If serialization is unavoidable, use strict integrity checks, narrow `allowed_classes`, and schema-based reconstruction. - Add regression tests that insert an unsigned/tampered queue item with a serialized `Job` and assert that the scheduler does not execute it. - Review other flat-file write paths to ensure untrusted users cannot create scheduler queue, cache, or session payloads. ## Additional Notes - Idempotency confirmation: `bundle/repro/reproduction_steps.sh` was executed twice consecutively and produced the same vulnerable/fixed divergence both times. - The proof uses the real Grav product, real Composer dependencies, real scheduler-webhook plugin, real local HTTP requests, and real `JobQueue::reconstructJob()` deserialization. It does not mock Grav classes or reimplement the sink. - The queue-file placement models the advisory precondition for the JobQueue vector: attacker-planted or tampered queue data. The remote/API boundary is the real webhook trigger that causes the product to process that queue item. ## Reproduction Details Reproduced: 2026-07-06T08:17:46.038Z Duration: 3200 seconds Tool calls: 359 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00233 pruva-verify CVE-2026-56700 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00233&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00233/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-56700 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 21004 bytes) - bundle/repro/rca_report.md (analysis, 6672 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 9341 bytes) - bundle/vuln_variant/rca_report.md (analysis, 9570 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 719 bytes) - bundle/repro/validation_verdict.json (other, 885 bytes) - bundle/repro/runtime_manifest.json (other, 4261 bytes) - bundle/repro/proof_vulnerable_1.txt (other, 38 bytes) - bundle/repro/proof_vulnerable_2.txt (other, 38 bytes) - bundle/logs/fixed_composer_1.log (log, 7910 bytes) - bundle/logs/fixed_composer_2.log (log, 7910 bytes) - bundle/logs/fixed_filecache_gap.log (log, 366 bytes) - bundle/logs/fixed_health_1_body.txt (other, 46 bytes) - bundle/logs/fixed_health_1_headers.txt (other, 523 bytes) - bundle/logs/fixed_health_2_body.txt (other, 46 bytes) - bundle/logs/fixed_health_2_headers.txt (other, 523 bytes) - bundle/logs/fixed_jobqueue_gap.log (log, 367 bytes) - bundle/logs/fixed_queue_files_after_1.txt (other, 124 bytes) - bundle/logs/fixed_queue_files_after_2.txt (other, 124 bytes) - bundle/logs/fixed_scheduler_1.log (log, 373 bytes) - bundle/logs/fixed_scheduler_2.log (log, 373 bytes) - bundle/logs/fixed_scheduler_health_1_body.json (other, 315 bytes) - bundle/logs/fixed_scheduler_health_1_headers.txt (other, 432 bytes) - bundle/logs/fixed_scheduler_health_2_body.json (other, 315 bytes) - bundle/logs/fixed_scheduler_health_2_headers.txt (other, 432 bytes) - bundle/logs/fixed_server_1.log (log, 587 bytes) - bundle/logs/fixed_server_2.log (log, 587 bytes) - bundle/logs/fixed_session_gap.log (log, 460 bytes) - bundle/logs/fixed_webhook_curl_1.log (log, 0 bytes) - bundle/logs/fixed_webhook_curl_2.log (log, 0 bytes) - bundle/logs/fixed_webhook_request_1.txt (other, 292 bytes) - bundle/logs/fixed_webhook_request_2.txt (other, 292 bytes) - bundle/logs/fixed_webhook_response_1_body.json (other, 141 bytes) - bundle/logs/fixed_webhook_response_1_headers.txt (other, 432 bytes) - bundle/logs/fixed_webhook_response_2_body.json (other, 141 bytes) - bundle/logs/fixed_webhook_response_2_headers.txt (other, 432 bytes) - bundle/logs/payload_fixed_1.generate.log (log, 23 bytes) - bundle/logs/payload_fixed_2.generate.log (log, 23 bytes) - bundle/logs/payload_vulnerable_1.generate.log (log, 23 bytes) - bundle/logs/payload_vulnerable_2.generate.log (log, 23 bytes) - bundle/logs/vulnerable_composer_1.log (log, 7909 bytes) - bundle/logs/vulnerable_composer_2.log (log, 7909 bytes) - bundle/logs/vulnerable_filecache_gap.log (log, 76 bytes) - bundle/logs/vulnerable_health_1_body.txt (other, 46 bytes) - bundle/logs/vulnerable_health_1_headers.txt (other, 523 bytes) - bundle/logs/vulnerable_health_2_body.txt (other, 46 bytes) - bundle/logs/vulnerable_health_2_headers.txt (other, 523 bytes) - bundle/logs/vulnerable_jobqueue_gap.log (log, 89 bytes) - bundle/logs/vulnerable_queue_files_after_1.txt (other, 129 bytes) - bundle/logs/vulnerable_queue_files_after_2.txt (other, 129 bytes) - bundle/logs/vulnerable_scheduler_1.log (log, 347 bytes) - bundle/logs/vulnerable_scheduler_2.log (log, 347 bytes) - bundle/logs/vulnerable_scheduler_health_1_body.json (other, 315 bytes) - bundle/logs/vulnerable_scheduler_health_1_headers.txt (other, 432 bytes) - bundle/logs/vulnerable_scheduler_health_2_body.json (other, 315 bytes) - bundle/logs/vulnerable_scheduler_health_2_headers.txt (other, 432 bytes) - bundle/logs/vulnerable_server_1.log (log, 587 bytes) - bundle/logs/vulnerable_server_2.log (log, 587 bytes) - bundle/logs/vulnerable_session_gap.log (log, 116 bytes) - bundle/logs/vulnerable_webhook_curl_1.log (log, 0 bytes) - bundle/logs/vulnerable_webhook_curl_2.log (log, 0 bytes) - bundle/logs/vulnerable_webhook_request_1.txt (other, 302 bytes) - bundle/logs/vulnerable_webhook_request_2.txt (other, 302 bytes) - bundle/logs/vulnerable_webhook_response_1_body.json (other, 141 bytes) - bundle/logs/vulnerable_webhook_response_1_headers.txt (other, 432 bytes) - bundle/logs/vulnerable_webhook_response_2_body.json (other, 141 bytes) - bundle/logs/vulnerable_webhook_response_2_headers.txt (other, 432 bytes) - bundle/logs/webhook_plugin_commit.log (log, 41 bytes) - bundle/logs/webhook_plugin_fetch.log (log, 108 bytes) - bundle/repro/payload_fixed_1.b64 (other, 2376 bytes) - bundle/repro/payload_fixed_1.generate.log (log, 23 bytes) - bundle/repro/payload_fixed_2.b64 (other, 2376 bytes) - bundle/repro/payload_fixed_2.generate.log (log, 23 bytes) - bundle/repro/payload_vulnerable_1.b64 (other, 2392 bytes) - bundle/repro/payload_vulnerable_1.generate.log (log, 23 bytes) - bundle/repro/payload_vulnerable_2.b64 (other, 2392 bytes) - bundle/repro/payload_vulnerable_2.generate.log (log, 23 bytes) - bundle/repro/queue_fixed_1.json (other, 2688 bytes) - bundle/repro/queue_fixed_2.json (other, 2688 bytes) - bundle/repro/queue_vulnerable_1.json (other, 2714 bytes) - bundle/repro/queue_vulnerable_2.json (other, 2714 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6339 bytes) - bundle/vuln_variant/variant_manifest.json (other, 3476 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3056 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1398 bytes) - bundle/logs/vuln_variant/reproduction_result.json (other, 543 bytes) - bundle/vuln_variant/proof_vulnerable.txt (other, 28 bytes) - bundle/vuln_variant/proof_fixed.txt (other, 23 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00233 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00233/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00233 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev