Skip to content

Quarterly GitHub Actions security audit — 2026 Q3 #29

Description

@smartwatermelon

Quarterly GitHub Actions Security Audit — 2026 Q3

Audit date: 2026-07-01
Methodology: docs/github-actions-security-methodology.md
Prior audit: 2026-04-29 Q2 auto — dev-env#23
Repos scanned: 31 (20 smartwatermelon + 11 nightowlstudiollc) — up from 29 in Q2
Repo changes since Q2: smartwatermelon/lamatic added; nightowlstudiollc/reliquarist added; nightowlstudiollc/juliet-cleaning renamed → nightowlstudiollc/tnjcleaning


Findings Table

Pattern Description Affected Repos Severity Status vs Q2
P7 issue_comment trigger without author_association gate swm/archive-resolver, swm/lock-sync, swm/ralph-burndown, swm/scripts, swm/claude-config, swm/spokane-snow, swm/.github, nos/.github Medium NEW (Q2 audit gap)
P2/P8 Lamatic/sync-flows-to-lamatic@v1 (mutable tag) in new repo swm/lamatic Medium NEW (new repo)
P2/P8 Cyb3r-Jak3/html5validator-action@v8.0.0 (mutable tag) in new repo nos/tnjcleaning Medium NEW (new repo)
P2/P8 dependabot/fetch-metadata@v3 (mutable tag) 26 repos (all with dependabot-auto-merge.yml) Low UNCHANGED — tracked in dev-env#19

Total new findings: 3 (all Medium). No new Critical or High.


Delta vs Q2 Audit (dev-env#23)

Category Count Details
NEW findings 3 P7 (8 repos, Medium); P2/P8 lamatic (Medium); P2/P8 tnjcleaning (Medium)
RESOLVED findings 0
UNCHANGED findings 1 P2/P8 dependabot/fetch-metadata@v3 — Low, Tier-3

Pattern-by-Pattern Results

Pattern 1 — pull_request_target + untrusted checkout

CLEAN. All pull_request_target hits are dependabot-auto-merge.yml at the same canonical SHA c53c7e3e300fd4fd2e5eddda56c928b1652aba31 in 26 repos. No checkout of PR-controlled code. Identical to Q2.

Pattern 2/8 — Mutable action references

UNCHANGED FINDING: dependabot/fetch-metadata@v3 in dependabot-auto-merge.yml across 26 repos. Severity: Low. Tracked in dev-env#19.

NEW FINDING — lamatic: smartwatermelon/lamatic (new repo) has .github/workflows/lamatic-update.yml using:

  • Lamatic/sync-flows-to-lamatic@v1 — third-party action, mutable semver tag. Medium.
  • actions/checkout@v4 — GitHub-owned, mutable tag. Low.

NEW FINDING — tnjcleaning: nightowlstudiollc/tnjcleaning (was juliet-cleaning) has .github/workflows/html-validation.yml using:

  • Cyb3r-Jak3/html5validator-action@v8.0.0 — third-party action, mutable semver tag. Medium.
  • actions/checkout@v7 — GitHub-owned, mutable tag. Low.

Additional consistent (Low) — not newly flagged: Multiple repos use GitHub-owned actions at mutable tags (actions/checkout@v7, actions/setup-node@v6, actions/setup-python@v6, pnpm/action-setup@v6) and anthropics/claude-code-action@v1 at a mutable tag. These are Low severity (first-party or well-maintained publishers) and consistent with the inaugural audit's tier-3 residual.

Pattern 3 — Cache poisoning

CLEAN. No actions/cache steps in any pull_request_target workflow.

Pattern 4 — Imposter commits from forks

NOT VERIFIED. Requires git reachability analysis not available in this environment. Same constraint as Q2.

Pattern 5 — Template injection in run: blocks

CLEAN. All ${{ github.event.* }} expressions in run: contexts use the safe env: isolation pattern (confirmed via code search across all 31 repos). Invariant holds.

Pattern 6 — Overpermissive default GITHUB_TOKEN

PARTIAL VERIFICATION — TOOL LIMITATION. The /repos/{owner}/{repo}/actions/permissions/workflow API endpoint is not reachable via MCP code search. Same constraint as Q2. The two repos flipped to read in the inaugural audit (ralph-burndown, mac-server-setup) are not expected to have regressed. Verify locally per methodology doc.

Pattern 7 — issue_comment without author_association gate

NEW FINDING. Eight repos have standalone claude.yml files that trigger on issue_comment WITHOUT the author_association sender-identity gate. Any GitHub user can @-mention Claude and trigger the workflow.

Affected repos (confirmed — author_association search returned 0 results in all 8):

Repo claude.yml SHA Notes
smartwatermelon/archive-resolver 5087689860eb Inline implementation, no reusable workflow
smartwatermelon/lock-sync be1946a4d197 Inline implementation
smartwatermelon/ralph-burndown cb5cde7c20dd Inline implementation
smartwatermelon/scripts 5087689860eb Inline implementation
smartwatermelon/claude-config 5087689860eb Inline implementation
smartwatermelon/spokane-snow 5087689860eb Inline implementation
smartwatermelon/.github e9838ad71150 Org-level default workflow — applies to all swm repos without their own claude.yml
nightowlstudiollc/.github edaee438b740 Org-level default workflow — applies to all nos repos without their own claude.yml

Why Q2 missed this: The Q2 audit incorrectly assumed all repos used the canonical reusable workflow (smartwatermelon/github-workflows/.github/workflows/claude-assistant.yml) and inherited its gate. These repos use an older inline implementation that predates the canonical pattern. This is likely a Q2 audit gap, not a post-Q2 regression.

Remediation — Option A (quick fix): Add the author_association check to the existing if: block:

# In .github/workflows/claude.yml — current (INSECURE):
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      ...

# Remediated:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association))

Remediation — Option B (preferred, aligns with canonical pattern): Migrate to the reusable workflow call (same as how swift-progress-indicator, mac-server-setup, slack-mcp, and others are set up):

# .github/workflows/claude.yml
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association))
    uses: smartwatermelon/github-workflows/.github/workflows/claude-assistant.yml@v3
    secrets:
      claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

Apply this to all 8 affected repos (or at minimum the 6 non-.github repos; the .github defaults are lower priority if most repos have their own files).

Pattern 9 — Missing workflow permissions: block

CLEAN for canonical workflows and new repos. nightowlstudiollc/reliquarist/claude.yml has explicit permissions: { contents: read, pull-requests: read, issues: read, id-token: write }. The inline claude.yml files in the 8 Pattern-7 repos were not fully verified (MCP file read is scoped to smartwatermelon/dev-env), but resolving Pattern 7 via Option B remediation would also bring permissions into the canonical state.


Draft PRs

0 draft PRs created. MCP GitHub write access is scoped to smartwatermelon/dev-env only in this session; cross-repo PR creation fails with access-denied.

  • PR creation skipped for smartwatermelon/archive-resolver: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/lock-sync: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/ralph-burndown: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/scripts: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/claude-config: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/spokane-snow: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/.github: MCP write scope limited to dev-env
  • PR creation skipped for nightowlstudiollc/.github: MCP write scope limited to dev-env
  • PR creation skipped for smartwatermelon/lamatic: MCP write scope limited to dev-env
  • PR creation skipped for nightowlstudiollc/tnjcleaning: MCP write scope limited to dev-env

Use the inline remediation snippets above to apply fixes manually (branch + PR in each affected repo).


Audit Infrastructure Notes

Same constraints as Q2:

  1. gh CLI unavailable — Pattern 6 (default workflow permissions) cannot be verified via API. Mark as PARTIAL pending local run.
  2. MCP write scope — Limited to smartwatermelon/dev-env; prevents automated draft PRs in other repos.

The 3 new Medium findings this quarter (vs 0 new in Q2) warrant tracking the resolution timeline. If manually applying remediations for the P7 finding across 8 repos proves slow, consider upgrading the routine environment to one with broader MCP write scope or gh CLI access.


Audit History Update

Date Repos Findings (severity) PRs opened Issues filed
2026-07-01 (Q3, auto) 31 0 critical, 0 high, 3 medium (new), 1 low (unchanged) 0 1 (this)
2026-04-29 (Q2, auto) 29 0 new (1 unchanged Low) 0 1 (dev-env#23)
2026-04-29 (inaugural, manual) 29 0 critical, 4 high, 10 medium 9 5

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions