gh-aw: safe-output validator forwards undeclared agent fields to the appliers (scope escape / mass assignment)
Critical9.1Published Aug 8, 2026
### Summary Agent safe-outputs are NDJSON validated by a trusted job before they are applied. The validator spreads the raw agent item and validates only the schema-declared fields, so any field the agent adds that the validator does not declare is forwarded to the applier verbatim. Wherever an applier reads such a field, the agent controls it. This is a mass-assignment class that yields several scope escapes, including overwriting another user's comment and retargeting or closing arbitrary pull requests. I verified this against current `main` at commit `966ccb8` (2026-07-12). It was first verified at `bdfdb8e` (2026-06-24, ~v0.80.9); the safe-outputs refactors in between changed only Go config-parsing modules and did not alter the field handling. ### Details In `validateItem`, the normalized item is built as `const normalizedItem = { ...item }` (`actions/setup/js/safe_output_type_validator.cjs:680`), which spreads the raw agent item. The validation loop then iterates only the schema-declared fields (`:692-707`) and returns the spread item unchanged for everything else (`:713`). Undeclared fields are not stripped; `actions/setup/js/collect_ndjson_output.cjs` pushes the item (`:...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| github/gh-aw Product | < v0.86.1 | v0.86.1 |
Details and references
### Summary Agent safe-outputs are NDJSON validated by a trusted job before they are applied. The validator spreads the raw agent item and validates only the schema-declared fields, so any field the agent adds that the validator does not declare is forwarded to the applier verbatim. Wherever an applier reads such a field, the agent controls it. This is a mass-assignment class that yields several scope escapes, including overwriting another user's comment and retargeting or closing arbitrary pull requests. I verified this against current `main` at commit `966ccb8` (2026-07-12). It was first verified at `bdfdb8e` (2026-06-24, ~v0.80.9); the safe-outputs refactors in between changed only Go config-parsing modules and did not alter the field handling. ### Details In `validateItem`, the normalized item is built as `const normalizedItem = { ...item }` (`actions/setup/js/safe_output_type_validator.cjs:680`), which spreads the raw agent item. The validation loop then iterates only the schema-declared fields (`:692-707`) and returns the spread item unchanged for everything else (`:713`). Undeclared fields are not stripped; `actions/setup/js/collect_ndjson_output.cjs` pushes the item (`:330`) and writes it to the agent output file (`:344`) with the extra fields intact. Confirmed instances (applier reads a field absent from the validator schema in `pkg/workflow/safe_outputs_validation_config.go`): - `add_comment` -> `comment_id`. Declared on the MCP tool but absent from the validator config; reaches `issues.updateComment` in `actions/setup/js/add_comment.cjs` with a positive-integer check and no ownership check, so a steered agent overwrites any comment by id. Tool-reachable on any engine because the tool schema exposes `comment_id`. - `update_pull_request` -> `base`, `state`. Reaches `pulls.update` in `actions/setup/js/update_pull_request.cjs` with no `allowed_base_branches` gate on the update path, so the agent retargets a PR's base branch or closes arbitrary PRs. - `upload_asset` -> `targetFileName`. Used as the destination of `fs.copyFileSync` with no containment (`actions/setup/js/upload_assets.cjs:131,163`), giving a runner filesystem write. Writing into `.git/hooks` or `.git/config` escalates to code execution in the write-token posting job. - `create_issue` -> `assignees`; `create_discussion` -> `labels`; `close_issue` -> `state_reason`; `push_to_pull_request_branch` -> `diff_size` (forging `0` skips the `max_patch_size` guard). All via the raw-NDJSON path. ### PoC For the `add_comment` case I set `comment_id` on the NDJSON item and overwrote another user's comment by id on my own repo, then reverted. For `update_pull_request` I retargeted a PR base branch on my own repo, then reverted. The remaining instances were confirmed by tracing each applier against the validator config. ### Impact A confused-deputy family: the agent influences state that the safe-output contract was meant to gate (comment integrity, PR base branch, PR state, runner filesystem). One fix closes the class. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:L (High). Suggested vector; adjust per instance. ### Suggested fix In `validateItem`, build `normalizedItem` from the type's declared schema fields only, instead of spreading `{ ...item }`.
More GitHub advisories
All GitHub| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 7 | gh-aw: unauthenticated prompt-injection to code execution in the shipped ai-moderator workflow | Medium6.5 | v0.86.1 |
| Aug 7 | command injection in compiled workflow via unsanitized `sandbox.mcp.env` exports | Critical9.6 | v0.86.0 |
| Aug 7 | gh-aw: URL allowlist bypass via userinfo @ in the content sanitizer (exfiltration channel) | Medium6.8 | v0.86.0 |
| Aug 6 | Safe-outputs config emitter: JSON injection via templated values despite env-var indirection | High8.5 | v0.78.0 |
| Aug 5 | GitHub Enterprise Server: denial of service | Medium6.6 | No fix yet |
| Aug 5 | GitHub Enterprise Server: path traversal | High8.8 | No fix yet |