Answer the two high-severity workflow-security findings - #378
Merged
Conversation
A workflow-security audit reports `if: github.actor == 'dependabot[bot]'` as a spoofable actor check. Reading it against the sibling workflow turned up a different, larger hole than the one reported. `github.actor` names whoever triggered the run. A push by someone else therefore made this job SKIP — and auto-merge, once enabled, survives later pushes to the head branch. The armed auto-merge stayed armed on a tip nobody re-checked, so a commit appended by anyone with push access could ride an approval it never got. Keying on the pull request's author instead — a fact no push can change — makes the job run on every event of a Dependabot pull request, which is exactly when it needs to act. dependabot/fetch-metadata is a real second gate and was never bypassed by the actor check: reading its source, it re-checks the pull request author, the FIRST commit's author and that commit's signature, never consults github.actor, and fails closed by emitting no outputs (both its skip-*-verification inputs default to false). What it does not check is the TIP, which is what auto-merge acts on. So the head is inspected here, and the two guards are deliberately asymmetric. ARMING requires Dependabot's own GitHub-signed commit — author names forge freely, the signature does not. WITHDRAWING triggers on the weaker signal, an author that is not Dependabot, because withdrawing is the fail-safe direction: at worst a human merges by hand. Keying the withdrawal on the signature instead would fight dependabot-autofix, which deliberately keeps auto-merge on after a trivial fix — its --amend and rebase preserve Dependabot as the author but drop the signature, and that case is now left alone rather than disarmed. Verified with the auditor: the finding is gone, no new one appears, and actionlint still passes on both files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
The same audit reports this trigger as a privilege-escalation vector, and the pattern generally is one. It is kept, and the reasoning now sits next to it rather than in a review thread. The trigger is not replaceable: a `pull_request` run raised by Dependabot gets a read-only token and no secrets, so it could neither read ANTHROPIC_API_KEY nor push the repair, and no other trigger grants both. The escalation it describes is already closed: the checkout is the BASE and never the pull request, the repair does only git operations so the bumped dependency is never built, and identity is settled against the API with the branch tip required to be Dependabot's own signed commit. That last check is stricter than dependabot/fetch-metadata, which validates the first commit rather than the tip — the gap the sibling workflow just had to close for itself. Silenced for the auditor with an inline directive carrying its reason, per ADR-0060, so the refusal is answered where it fires and not re-litigated the next time the audit runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
The automerge reference documented `github.actor` in three places, including a "the actor guard matters" bullet that is now the opposite of the rule. It describes the author guard instead, why it must not go back to the actor, the head classification and the withdrawal step, and the asymmetry between arming and withdrawing — with the reason the symmetric version would break the sibling workflow, so nobody tidies it into one check. The autofix reference gains one bullet: the audit reports its `workflow_run` trigger, and the answer — not replaceable, escalation already closed, refusal recorded inline — belongs beside the guards it builds on. English and French both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
Reefact
force-pushed
the
claude/sonar-points-restants-12zlnr
branch
from
July 29, 2026 18:11
0ff14a1 to
f1a6dca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
zizmoraudit of the 18 workflows reported two HIGH findings. One is declined with its reason recorded in the file; the other turned out to mark a larger hole than the one reported, and the reported line was making it worse.Type of change
Changes
dependabot-automerge.yml— fixedThe finding was
if: github.actor == 'dependabot[bot]', reported as a spoofable actor check. Reading it against the sibling workflow found the real problem:github.actornames whoever triggered the run, so a push by somebody else made the job skip. Auto-merge, once enabled, survives later pushes to the head branch — so skipping left it armed on a tip nobody re-checked. A commit appended by anyone with push access could ride an approval it never got.github.event.pull_request.user.login— the author, a fact no push can change — so it runs on every event of a Dependabot PR, which is exactly when it needs to act. The change is not cosmetic: it is what makes the rest possible.--disable-auto) triggers on a foreign head.The two guards are asymmetric on purpose. Arming takes the strong proof — commit author names are
git configvalues and forge freely, GitHub's signature does not. Withdrawing takes the weaker signal, because withdrawing is the fail-safe direction: at worst a human merges by hand.That asymmetry is a necessity, not a flourish. Keying the withdrawal on the signature would fight
dependabot-autofix, which deliberately keeps auto-merge on after a trivial fix: its--amendandrebasepreserve Dependabot as the author but drop the signature. That case is now left alone rather than disarmed. Verified againsttools/dependabot-autofix/apply-fix.shbefore choosing.dependabot-autofix.yml— declined, in writingdangerous-triggersonworkflow_run. Kept, because:pull_requestrun raised by Dependabot gets a read-only token and no secrets, so it could neither readANTHROPIC_API_KEYnor push the repair.dependabot/fetch-metadata, which validates the first commit — the very gap the sibling workflow just had to close for itself.Recorded as an inline
# zizmor: ignore[dangerous-triggers]with its reason above it, per ADR-0060.Documentation
The automerge reference documented
github.actorin three places, including a "the actor guard matters" bullet that is now the opposite of the rule. Rewritten in English and French, plus a bullet on the autofix side. Both say explicitly why the asymmetric guards must not be tidied into one.Testing
dotnet build FirstClassErrors.slndotnet test FirstClassErrors.slnFirstClassErrors.Analyzers.UnitTests)Zero warnings, 2115 tests passing. No C# changes here, so that is a regression check rather than the relevant one.
What actually verifies this change:
zizmor: 0 HIGH, down from 2.bot-conditionsis resolved by the fix;dangerous-triggersreports as1 ignored, which confirms the inline directive is honoured (its placement was corrected after the first attempt failed).lint.yml's own checkout, merged in ci: lint the scripts and workflows on every pull request #376, not this diff.actionlintclean on both files,shellcheckclean, both YAML files parse and still expose the same trigger keys.Documentation
doc/updateddoc/handwritten/for-users/README.fr.md) updated if user-facing behavior changedWorkflow reference updated in English and French.
README.fr.mdunderfor-usersis untouched — nothing user-facing changes.Architecture decisions
Proposed: ADR-____The inline refusal follows ADR-0060's existing rule (record the refusal beside what it refuses); it does not add a decision.
Notes for the reviewer
One thing I cannot check, and it sets the urgency rather than the fix. If branch protection on
mainrequires an approving review and dismisses stale approvals on new commits, the path described above was already blocked by that. I have no access to the branch-protection settings. The fix is right either way; whether it was closing an open door or bolting a closed one is yours to confirm.What reading beat trusting the tool.
dependabot/fetch-metadatawas a real second gate — its source re-checks the PR author, the first commit's author and that commit's signature, never consultsgithub.actor, and fails closed by emitting no outputs (bothskip-*-verificationinputs default tofalse). So the reported HIGH overstated the risk of the actor line taken alone. What the action does not check is the tip, and the tip is what auto-merge merges. Correcting only the flagged line would have left the real defect in place.Scope. These two findings only.
zizmoralso reports 24artipacked(medium) and 10template-injection(informational), and is not adopted into CI here — both were deliberately left out.Related issues
None.
Generated by Claude Code