## Summary
No in-scope distinct bypass of the Grav CMS 2.0.0-beta.2 fix was confirmed. The variant stage inspected the three unsafe deserialization sinks named in the ticket (`Scheduler\JobQueue`, `Framework\Cache\Adapter\FileCache`, and `Session::getFlashObject`) plus the command-injection fix in `Console\Cli\InstallCommand`. Runtime testing then exercised the most bypass-like `JobQueue` candidate: a queue item whose attacker-controlled `serialized_job` is accompanied by a valid HMAC. That candidate executes on both vulnerable 2.0.0-beta.1 and fixed 2.0.0-beta.2, but only because the reproducer signs the payload with the server-side `Security::getNonceKey()` in the same local site context. It therefore does not cross the same remote/API trust boundary as the parent proof and is not a valid security bypass.

## 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 by `JobQueue::push()` include `serialized_job_hmac`; `JobQueue::reconstructJob()` verifies the HMAC before unserializing `serialized_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 versioned `v2` envelope 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()` stores `v2|<hmac>|<serialized>`; `getFlashObject()` only unserializes when that HMAC verifies.
- `system/src/Grav/Console/Cli/InstallCommand.php`: untrusted `.dependencies` values (`branch`, `url`, and `path`) are passed through `escapeshellarg()`, and a `--` separator is inserted before url/path for `git clone` option-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\JobQueue` `serialized_job` payload.
- 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.php` creates a real `Grav\Common\Scheduler\Job('system', [...])`, serializes it, and writes a queue JSON item containing both `serialized_job` and `serialized_job_hmac`. It then calls `JobQueue::pop()` so the fixed `JobQueue::reconstructJob()` HMAC branch is reached.
- Source anchor: `system/src/Grav/Common/Scheduler/JobQueue.php`, especially `push()` around the HMAC writer and `reconstructJob()` 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:
- `FileCache` unsigned legacy payloads are explicitly rejected by the `v2` format/HMAC check before `unserialize()`.
- `Session::getFlashObject` unsigned legacy payloads and forged-HMAC payloads are rejected before `unserialize()`.
- The `.dependencies` command-injection path in `InstallCommand::gitclone()` now shell-quotes branch/url/path and uses `--`; no alternate unquoted clone path in the same command was found during the bounded scan.

## Impact
- Package/component affected: `getgrav/grav`, focusing on `Grav\Common\Scheduler\JobQueue` and related deserialization integrity fixes.
- Versions tested: vulnerable `2.0.0-beta.1` at `26a2d519c59c620e2b0a54d0baf33889d7d5db0a`; fixed/latest available tag `2.0.0-beta.2` at `f95b0ff51a655edcbcc060a3d74b43e3f20b9585`.
- 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: `none` for 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 `.dependencies` path 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
1. Run `bundle/vuln_variant/reproduction_steps.sh`.
2. The script prepares/reuses the Grav repository, checks out `2.0.0-beta.1` and `2.0.0-beta.2`, runs Composer install for each checkout, writes a minimal Grav site configuration, and executes a signed-HMAC `JobQueue` probe against both versions.
3. Expected evidence: the script exits `1` because no in-scope bypass is confirmed. It still writes proof markers showing the signed local candidate executes on both versions, and it writes `bundle/logs/vuln_variant/reproduction_result.json` with `bypass_security_scope:false`.

## Evidence
- Reproducer: `bundle/vuln_variant/reproduction_steps.sh`.
- Runtime result: `bundle/logs/vuln_variant/reproduction_result.json` records `vulnerable_signed_hmac_exec:true`, `fixed_signed_hmac_exec:true`, and `bypass_security_scope:false`.
- Fixed-version proof marker: `bundle/vuln_variant/proof_fixed.txt` contains `GRAV_VARIANT_HMAC_FIXED`.
- Vulnerable-version proof marker: `bundle/vuln_variant/proof_vulnerable.txt` contains `GRAV_VARIANT_HMAC_VULNERABLE`.
- Fixed probe stdout: `bundle/logs/vuln_variant/fixed_hmac_probe_stdout.json` shows `proof_created:true`, `class:"Grav\\Common\\Scheduler\\Job"`, `command:"system"`, and `hmac_source:"Security::getNonceKey() in the same local site context"`.
- Source scan and threat model excerpt: `bundle/logs/vuln_variant/source_scan.log` includes `SECURITY.md`, vulnerable/fixed `unserialize()` sink lists, and fixed command-injection context.
- Source identity: `bundle/logs/vuln_variant/source_identity.txt`, `bundle/logs/vuln_variant/vulnerable_version.txt`, and `bundle/logs/vuln_variant/fixed_version.txt` record 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_classes` allowlists 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\Process` with 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.
