CVE-2026-56700: Verified Repro With Script Download
CVE-2026-56700: Grav CMS unsafe deserialization and command injection RCE
CVE-2026-56700 is verified against the affected target. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00233.
What Is CVE-2026-56700?
CVE-2026-56700 is a critical unsafe-deserialization vulnerability in Grav CMS's modern scheduler job queue, letting a forged serialized scheduler Job object execute an attacker-selected command when scheduler processing is triggered. Pruva reproduced it (reproduction REPRO-2026-00233).
CVE-2026-56700 Severity & CVSS Score
CVE-2026-56700 is rated critical severity, with a CVSS base score of 9.8 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
How to Reproduce CVE-2026-56700
pruva-verify REPRO-2026-00233 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00233/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-56700
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
forged scheduler queue JSON file containing a base64 serialized Grav\Common\Scheduler\Job object; remote trigger is POST /scheduler/webhook
- POST /scheduler/webhook
- Scheduler::processWebhookTrigger()
- Scheduler::run()
- JobQueue::reconstructJob()
- unserialize(serialized_job)
- Job::run/system
reproduction_steps.sh No in-scope bypass confirmed. A signed JobQueue serialized_job reaches the fixed HMAC-verified unserialize path, but only when generated with the server-side Security::getNonceKey() in the local process; unsigned/tampered remote-style payloads are blocked by 2.0.0-beta.2.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-56700
- 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
Joband 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\Jobpayloads whose command writes marker files, starts a real Grav web server with the real scheduler-webhook plugin, and sendsPOST /scheduler/webhookto triggerScheduler::processWebhookTrigger()->Scheduler::run()->JobQueue::popWithId()/reconstructJob(). - Parity:
full. - Not demonstrated: the reproduction focuses on the
Scheduler\JobQueuevector. It does not separately exploit theFileCacheorSession::getFlashObjectunsafe unserialize sinks.
Root Cause
The vulnerable JobQueue::reconstructJob() path trusted the on-disk queue item field serialized_job and directly performed:
$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
- Run
bundle/repro/reproduction_steps.sh. - 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-webhookplugin, starts the real PHP/Grav web server, poisons the real scheduler queue with a forgedserialized_job, and sendsPOST /scheduler/webhookto trigger scheduler processing over HTTP. - Expected evidence: vulnerable 2.0.0-beta.1 creates
bundle/repro/proof_vulnerable_1.txtandbundle/repro/proof_vulnerable_2.txt; fixed 2.0.0-beta.2 does not create fixed proof files and logs execution of the benign/bin/truefallback job instead.
Evidence
- Runtime manifest:
bundle/repro/runtime_manifest.jsonrecordsentrypoint_kind="api_remote",service_started=true,healthcheck_passed=true, andtarget_path_reached=true. - Structured verdict:
bundle/repro/validation_verdict.jsonrecordsclaim_outcome="confirmed",validated_surface="api_remote", andobserved_impact_class="code_execution". - Vulnerable proof files:
bundle/repro/proof_vulnerable_1.txtcontainsGRAV_JOBQUEUE_WEBHOOK_RCE_vulnerable_1.bundle/repro/proof_vulnerable_2.txtcontainsGRAV_JOBQUEUE_WEBHOOK_RCE_vulnerable_2.
- Vulnerable HTTP evidence:
bundle/logs/vulnerable_webhook_response_1_body.jsonshows the webhook returned success andjobs_run: 1.bundle/logs/vulnerable_scheduler_1.logandbundle/logs/vulnerable_scheduler_2.logshow the forged job completed successfully with commandsystem.
- Fixed negative control:
bundle/logs/fixed_scheduler_1.logandbundle/logs/fixed_scheduler_2.logshow the fixed version completed the benign fallback job (/bin/true) rather than the attacker-controlled serializedJob.- No
proof_fixed_*.txtfiles are produced.
- Patch-gap evidence:
bundle/logs/vulnerable_jobqueue_gap.logcontains the vulnerable unsignedunserialize(base64_decode(...))sink.bundle/logs/fixed_jobqueue_gap.logcontains 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.
- Grav vulnerable tag: 2.0.0-beta.1 (
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
Joband 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.shwas 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.
Variant Analysis & Alternative Triggers for CVE-2026-56700
Fix Coverage / Assumptions
The original fix relies on integrity, not on class filtering alone. The fixed code still uses unserialize(..., ['allowed_classes' => true]) for legitimate persisted Grav objects, but only after verifying that the serialized bytes were produced by the same site using a per-site HMAC key.
Covered code paths:
system/src/Grav/Common/Scheduler/JobQueue.php: queue items written byJobQueue::push()includeserialized_job_hmac;JobQueue::reconstructJob()verifies the HMAC before unserializingserialized_job. Unsigned or mismatched queue items fall back to structured fields instead of trusting the serialized object.system/src/Grav/Framework/Cache/Adapter/FileCache.php: file-cache entries use a versionedv2envelope and HMAC over the serialized value. Pre-v2, tampered, or stale-key files are treated as cache misses and deleted.system/src/Grav/Common/Session.php:setFlashObject()storesv2|<hmac>|<serialized>;getFlashObject()only unserializes when that HMAC verifies.system/src/Grav/Console/Cli/InstallCommand.php: untrusted.dependenciesvalues (branch,url, andpath) are passed throughescapeshellarg(), and a--separator is inserted before url/path forgit cloneoption-injection resistance.
The fix does not try to block same-site code or an actor who already has the private HMAC key. That is an intentional assumption: if an attacker can read/use Security::getNonceKey() or can run arbitrary PHP in the Grav process to sign a malicious serialized object, the original trust boundary has already been crossed. Grav's SECURITY.md also distinguishes severity by the level of access required; this candidate would require local/same-process privilege, not the no-account remote/API boundary demonstrated by the parent reproduction.
Variant / Alternate Trigger
Tested variant candidate:
- Candidate: signed
Scheduler\JobQueueserialized_jobpayload. - Entry point tested: direct queue processing through
JobQueue::pop()in a CLI probe under a real checked-out Grav instance. - Code path:
variant_jobqueue_hmac_probe.phpcreates a realGrav\Common\Scheduler\Job('system', [...]), serializes it, and writes a queue JSON item containing bothserialized_jobandserialized_job_hmac. It then callsJobQueue::pop()so the fixedJobQueue::reconstructJob()HMAC branch is reached. - Source anchor:
system/src/Grav/Common/Scheduler/JobQueue.php, especiallypush()around the HMAC writer andreconstructJob()around the HMAC verifier/unserialize branch.
Outcome: The candidate executed on the fixed version, but it is not a valid bypass because the payload was signed by the server-side secret in the same local process. An unauthenticated remote/API attacker who can only plant or tamper with a queue file cannot compute the HMAC; the parent exploit's unsigned serialized job is rejected by the fixed version.
Other candidate review:
FileCacheunsigned legacy payloads are explicitly rejected by thev2format/HMAC check beforeunserialize().Session::getFlashObjectunsigned legacy payloads and forged-HMAC payloads are rejected beforeunserialize().The
.dependenciescommand-injection path inInstallCommand::gitclone()now shell-quotes branch/url/path and uses--; no alternate unquoted clone path in the same command was found during the bounded scan.Versions tested: vulnerable
2.0.0-beta.1at26a2d519c59c620e2b0a54d0baf33889d7d5db0a; fixed/latest available tag2.0.0-beta.2atf95b0ff51a655edcbcc060a3d74b43e3f20b9585.Risk level and consequences for the parent issue: critical remote/API-triggered code execution when an attacker can tamper with queued serialized jobs and trigger scheduler processing.
Risk level for the tested candidate: not an in-scope vulnerability, because exploitation requires access to the site's private HMAC key or same-process local signing capability.
Impact Parity
- Disclosed/claimed maximum impact for the parent: arbitrary code execution through unsafe deserialization and command injection in Grav before 2.0.0-beta.2.
- Reproduced impact from this variant run: code execution was observed only for the local signed-HMAC candidate on both vulnerable and fixed versions.
- Parity:
nonefor an in-scope bypass. The runtime effect is code execution, but the trust boundary and attacker preconditions do not match the parent vulnerability. - Not demonstrated: no unauthenticated remote/API exploit against the fixed version; no fixed-version unsigned/tampered FileCache, Session, JobQueue, or
.dependenciespath that bypasses the added integrity/escaping controls.
Root Cause
The parent root cause was trusting attacker-influenced serialized bytes (unserialize() on queue/cache/session data) without an integrity boundary. The fixed version changes that root cause by requiring HMAC integrity for every named serialized-data sink before unserialization.
The signed-HMAC probe reaches the same JobQueue::reconstructJob() sink in 2.0.0-beta.2, but it does so by satisfying the new invariant with the site's private key. That is expected behavior for legitimate queue entries, not a bypass of the fix. The same root cause is therefore not exploitable across the original trust boundary.
Fix release/tag tested: 2.0.0-beta.2 (f95b0ff51a655edcbcc060a3d74b43e3f20b9585). The source scan also records the changelog/security context under bundle/logs/vuln_variant/source_scan.log.
Reproduction Steps
- Run
bundle/vuln_variant/reproduction_steps.sh. - The script prepares/reuses the Grav repository, checks out
2.0.0-beta.1and2.0.0-beta.2, runs Composer install for each checkout, writes a minimal Grav site configuration, and executes a signed-HMACJobQueueprobe against both versions. - Expected evidence: the script exits
1because no in-scope bypass is confirmed. It still writes proof markers showing the signed local candidate executes on both versions, and it writesbundle/logs/vuln_variant/reproduction_result.jsonwithbypass_security_scope:false.
Evidence
- Reproducer:
bundle/vuln_variant/reproduction_steps.sh. - Runtime result:
bundle/logs/vuln_variant/reproduction_result.jsonrecordsvulnerable_signed_hmac_exec:true,fixed_signed_hmac_exec:true, andbypass_security_scope:false. - Fixed-version proof marker:
bundle/vuln_variant/proof_fixed.txtcontainsGRAV_VARIANT_HMAC_FIXED. - Vulnerable-version proof marker:
bundle/vuln_variant/proof_vulnerable.txtcontainsGRAV_VARIANT_HMAC_VULNERABLE. - Fixed probe stdout:
bundle/logs/vuln_variant/fixed_hmac_probe_stdout.jsonshowsproof_created:true,class:"Grav\\Common\\Scheduler\\Job",command:"system", andhmac_source:"Security::getNonceKey() in the same local site context". - Source scan and threat model excerpt:
bundle/logs/vuln_variant/source_scan.logincludesSECURITY.md, vulnerable/fixedunserialize()sink lists, and fixed command-injection context. - Source identity:
bundle/logs/vuln_variant/source_identity.txt,bundle/logs/vuln_variant/vulnerable_version.txt, andbundle/logs/vuln_variant/fixed_version.txtrecord the tested revisions. - Idempotency: the variant reproducer was executed twice successfully as a script; each run completed and returned the expected stage result (
exit 1, meaning no valid bypass).
Recommendations / Next Steps
- Treat the HMAC requirement as security-critical and preserve it for every serialized queue/cache/session payload.
- Add regression tests for unsigned, forged-HMAC, malformed-base64, and correct-HMAC queue/cache/session objects. The correct-HMAC case should be documented as legitimate same-site behavior, not an attacker bypass.
- Keep the HMAC key (
Security::getNonceKey()/user/config/security-private.php) outside the web-accessible and template-readable configuration surface. - Consider using schema-based reconstruction or
allowed_classesallowlists for additional hardening, but do not rely on allowlists alone for attacker-influenced persistent data. - For the command-injection path, keep shell arguments quoted and prefer
Symfony\Component\Process\Processwith argv arrays for future commands.
Additional Notes
- The only fixed-version code execution observed requires generating a valid HMAC with the server-side key. That is expected for legitimate persisted jobs and is outside the parent vulnerability's remote/API attacker model.
- The tested fixed/latest tag available in this repository is
2.0.0-beta.2; no newer tag was present in the local repository at test time.
CVE-2026-56700 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-56700
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-56700
FAQ: CVE-2026-56700
What access does an attacker need to exploit CVE-2026-56700?
Are other Grav components affected besides the scheduler job queue?
How severe is CVE-2026-56700?
How can I reproduce CVE-2026-56700?
References for CVE-2026-56700
Authoritative sources for CVE-2026-56700 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.