Skip to content

Answer the two high-severity workflow-security findings - #378

Merged
Reefact merged 3 commits into
mainfrom
claude/sonar-points-restants-12zlnr
Jul 29, 2026
Merged

Answer the two high-severity workflow-security findings#378
Reefact merged 3 commits into
mainfrom
claude/sonar-points-restants-12zlnr

Conversation

@Reefact

@Reefact Reefact commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

A zizmor audit 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

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

dependabot-automerge.yml — fixed

The finding was if: github.actor == 'dependabot[bot]', reported as a spoofable actor check. Reading it against the sibling workflow found the real problem:

  • github.actor names 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.
  • The job now keys on 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.
  • The head commit of the event is inspected and classified: Dependabot-signed, Dependabot-authored-but-unsigned, or foreign.
  • Arming now requires a Dependabot-signed head. Withdrawing (--disable-auto) triggers on a foreign head.

The two guards are asymmetric on purpose. Arming takes the strong proof — commit author names are git config values 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 --amend and rebase preserve Dependabot as the author but drop the signature. That case is now left alone rather than disarmed. Verified against tools/dependabot-autofix/apply-fix.sh before choosing.

dependabot-autofix.yml — declined, in writing

dangerous-triggers on workflow_run. Kept, because:

  • 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.
  • The escalation 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 is stricter than 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.actor in 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.sln
  • dotnet test FirstClassErrors.sln
  • Analyzer tests pass (FirstClassErrors.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-conditions is resolved by the fix; dangerous-triggers reports as 1 ignored, which confirms the inline directive is honoured (its placement was corrected after the first attempt failed).
  • No new finding introduced — the total moving 77 → 78 is lint.yml's own checkout, merged in ci: lint the scripts and workflows on every pull request #376, not this diff.
  • actionlint clean on both files, shellcheck clean, both YAML files parse and still expose the same trigger keys.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

Workflow reference updated in English and French. README.fr.md under for-users is untouched — nothing user-facing changes.

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-____
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: 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 main requires 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-metadata was a real second gate — its source re-checks the PR author, the first commit's author and that commit's signature, never consults github.actor, and fails closed by emitting no outputs (both skip-*-verification inputs default to false). 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. zizmor also reports 24 artipacked (medium) and 10 template-injection (informational), and is not adopted into CI here — both were deliberately left out.

Related issues

None.


Generated by Claude Code

claude added 3 commits July 29, 2026 18:10
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
Reefact force-pushed the claude/sonar-points-restants-12zlnr branch from 0ff14a1 to f1a6dca Compare July 29, 2026 18:11
@Reefact
Reefact merged commit e22628a into main Jul 29, 2026
29 checks passed
@Reefact
Reefact deleted the claude/sonar-points-restants-12zlnr branch July 29, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants