## 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.
