Summary
Deferred hardening findings for gh-workflow (github-workflow package), verified at HEAD 1922e320 under internal/ghworkflow/ during security read H13. These items were deliberately kept out of the v5.28.0 train because they need a 1.10 github-workflow cut. Line numbers below should be re-checked before acting on them — the tree has moved since the read.
Findings
- MED —
closepr.go ~297-314 / relation/evaluate.go ~507-528 / topology.go ~300 — Final-Disposition evidence is accepted from any comment author; the authoring actor is discarded. Exploit/failure scenario: a third party posts a comment that reads as a clear disposition, creating a permanent CONFLICT or silently overriding the operator's --reason. Fix: treat evidence as authoritative only when authored by the authenticated actor or an explicit allowlist.
- MED-LOW —
closepr.go ~182-183, relation/evaluate.go ~528, cli/envelope.go ~196-210, mutate/ready.go ~138 — untrusted GitHub text (%s, raw error text carrying up to 200 B of response body) reaches the envelope/terminal without SanitizeText; that sanitizer (render/safetext.go) is applied only to summary/receipt rendering. Failure scenario: attacker-controlled text (issue/PR body, comment, or API error body) is echoed unsanitized to the terminal or JSON envelope. Fix: move the sanitizer into a leaf package and apply it in cli.WriteEnvelope.
- LOW —
mutate/merge.go ~199 — EnableAutoMerge is called without expectedHeadOid. Failure scenario: a push lands after arming auto-merge and before the merge fires, so ungated content merges. Fix: pass the validated head SHA through to the auto-merge call.
- LOW —
mutate/ready.go ~159 — no conditional guard exists between the gate read and the mutation that follows it (TOCTOU window).
- LOW —
render/repo.go ~187-191 — the origin host is never compared against the API base host, so a non-GitHub origin can be made to write to a same-named github.com repository.
- LOW —
cli/cli.go ~298-314 — ResolveRepoFile can walk past the checkout root, so policy.toml / org-schema.yaml can be sourced from an ancestor directory outside the intended checkout.
- INFO —
audit/command.go ~161 — organization schema text is printed unsanitized.
- Operability (not security) — no retry /
Retry-After handling on HTTP 403/429; a rate-limit response is currently misreported as ErrUnauthorized.
Note: item #3 from the same H13 read (PR trailer forgery) was already fixed in 1.9 (#232). 1.9's release-class enforcement is declared-but-not-enforced by design per ADR 0031; enforcing it, if wanted, is itself a 1.10 candidate.
Acceptance criteria
Out of scope
Evidence / references
Provenance
security read H13, session 2026-09-01, deferred from v5.28.0 because it needs a 1.10 github-workflow cut.
Summary
Deferred hardening findings for
gh-workflow(github-workflow package), verified at HEAD1922e320underinternal/ghworkflow/during security read H13. These items were deliberately kept out of the v5.28.0 train because they need a 1.10github-workflowcut. Line numbers below should be re-checked before acting on them — the tree has moved since the read.Findings
closepr.go~297-314 /relation/evaluate.go~507-528 /topology.go~300 — Final-Disposition evidence is accepted from any comment author; the authoring actor is discarded. Exploit/failure scenario: a third party posts a comment that reads as a clear disposition, creating a permanent CONFLICT or silently overriding the operator's--reason. Fix: treat evidence as authoritative only when authored by the authenticated actor or an explicit allowlist.closepr.go~182-183,relation/evaluate.go~528,cli/envelope.go~196-210,mutate/ready.go~138 — untrusted GitHub text (%s, raw error text carrying up to 200 B of response body) reaches the envelope/terminal withoutSanitizeText; that sanitizer (render/safetext.go) is applied only to summary/receipt rendering. Failure scenario: attacker-controlled text (issue/PR body, comment, or API error body) is echoed unsanitized to the terminal or JSON envelope. Fix: move the sanitizer into a leaf package and apply it incli.WriteEnvelope.mutate/merge.go~199 —EnableAutoMergeis called withoutexpectedHeadOid. Failure scenario: a push lands after arming auto-merge and before the merge fires, so ungated content merges. Fix: pass the validated head SHA through to the auto-merge call.mutate/ready.go~159 — no conditional guard exists between the gate read and the mutation that follows it (TOCTOU window).render/repo.go~187-191 — the origin host is never compared against the API base host, so a non-GitHub origin can be made to write to a same-namedgithub.comrepository.cli/cli.go~298-314 —ResolveRepoFilecan walk past the checkout root, sopolicy.toml/org-schema.yamlcan be sourced from an ancestor directory outside the intended checkout.audit/command.go~161 — organization schema text is printed unsanitized.Retry-Afterhandling on HTTP 403/429; a rate-limit response is currently misreported asErrUnauthorized.Note: item #3 from the same H13 read (PR trailer forgery) was already fixed in 1.9 (#232). 1.9's release-class enforcement is declared-but-not-enforced by design per ADR 0031; enforcing it, if wanted, is itself a 1.10 candidate.
Acceptance criteria
--reason.cli.WriteEnvelope(including raw API error bodies) passes through a sanitizer applied at that leaf, not only in summary/receipt rendering.EnableAutoMergepasses the validatedexpectedHeadOidso a late push cannot merge ungated content.mutate/ready.gois closed with an appropriate conditional guard, or the residual race is explicitly documented as accepted.ResolveRepoFilecannot resolve a path outside the checkout root.audit/command.gois sanitized before output.Retry-Afterhandling and are no longer misreported asErrUnauthorized.Out of scope
Evidence / references
1922e320, files underinternal/ghworkflow/(line numbers approximate; re-check before citing further, the tree has moved since the read).Provenance
security read H13, session 2026-09-01, deferred from v5.28.0 because it needs a 1.10
github-workflowcut.