Skip to content

Add GHA supply chain security rules (mutable-action-tag, pwn-request)#3783

Merged
inkz merged 11 commits intodevelopfrom
feat/gha-supply-chain-rules
Mar 31, 2026
Merged

Add GHA supply chain security rules (mutable-action-tag, pwn-request)#3783
inkz merged 11 commits intodevelopfrom
feat/gha-supply-chain-rules

Conversation

@kurt-r2c
Copy link
Copy Markdown
Contributor

Summary

Two GitHub Actions supply chain security rules:

github-actions-mutable-action-tag (WARNING / CWE-1104)

Detects uses: references not pinned to a full 40-character commit SHA. Uses pattern-regex with negative lookahead to reject exact 40-char hex SHAs while catching tags (@v3), branches (@master), and other mutable refs.

TP examples: actions/checkout@v3, bridgecrewio/checkov-action@master, aquasecurity/trivy-action@0.29.0
TN examples: any @<40-hex-chars>, local ./.github/actions/, docker://

Corpus: 853 peer-vendor repos, 6,147 findings, 0 SHA-pinned false positives.

Motivation: trivy-action@0.69.3 and kics-github-action@master were silently repointed to malicious commits in the TeamPCP campaign (2026-03).

gha-pwn-request-fork-checkout (ERROR / CWE-829)

Detects the "Pwn Request" pattern: pull_request_target trigger + fork-controlled checkout ref (github.event.pull_request.head.sha, github.head_ref, or github.event.pull_request.head.ref). Higher severity and confidence than the existing pull-request-target-code-checkout rule; does not require actions/checkout specifically.

Confirmed TPs from investigation:

  • sigstore/community — CRITICAL (PULUMI_ACCESS_TOKEN controls entire sigstore GitHub org)
  • jfrog/jfrog-cli — HIGH (GITHUB_TOKEN write + FASTCI_TOKEN)
  • SonarSource/official-images — HIGH (GITHUB_TOKEN write)

Notes

  • Ported to OSS YAML from semgrep/semgrep-rules-jsonnet PRs #10484 and #10485 per reviewer feedback that community rules belong in this repository
  • gha-pwn-request-fork-checkout uses pattern-inside for trigger detection (replaces semgrep-internal-pattern-anywhere from the Jsonnet version) matching the approach used by the existing pull-request-target-code-checkout rule

🤖 Generated with Claude Code

github-actions-mutable-action-tag (WARNING / CWE-1104):
- Detects non-SHA-pinned uses: references via pattern-regex with negative
  lookahead for 40-char hex SHA. Catches all mutable refs including tagged
  versions, branches, and 'latest'. Validated against 853 peer-vendor repos
  (6,147 findings, 0 SHA-pinned false positives).
- Motivated by TeamPCP campaign where trivy-action@0.29.0 and
  kics-github-action@master were repointed to malicious commits.

gha-pwn-request-fork-checkout (ERROR / CWE-829):
- Detects pull_request_target + fork-controlled checkout ref combination
  (the "Pwn Request" attack pattern). Uses pattern-inside for trigger
  detection + metavariable-regex for fork ref forms.
- Confirmed TPs: sigstore/community (CRITICAL, PULUMI_ACCESS_TOKEN),
  jfrog/jfrog-cli (HIGH), SonarSource/official-images (HIGH).

Both ported from semgrep/semgrep-rules-jsonnet PRs #10484 and #10485
per reviewer feedback that community rules belong in this repository.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kurt-r2c kurt-r2c force-pushed the feat/gha-supply-chain-rules branch from 36aed10 to cd59bf3 Compare March 26, 2026 19:41
@kurt-r2c
Copy link
Copy Markdown
Contributor Author

Real-world TP confirmation: The root cause of the TeamPCP Phase 07 trivy supply chain attack has been confirmed as aquasecurity/trivy's apidiff.yaml workflow — a pull_request_target that checked out fork head code and ran it with access to ORG_REPO_TOKEN, VSCE_TOKEN, and OVSX_TOKEN. The attack occurred Feb 27, 2026; credentials were stolen within 44 minutes of a malicious PR being opened.

This is a direct real-world TP for gha-pwn-request-fork-checkout. The pattern this rule detects was the initial access vector for a multi-stage supply chain campaign that reached PyPI (litellm, telnyx) and involved force-pushing malicious tags to aquasec/trivy-action (75 tags) and checkmarx/kics-github-action.

kurt-r2c and others added 10 commits March 31, 2026 09:59
…ckout

Absorbs the new rule's improvements into the existing rule rather than
shipping a duplicate rule for the same vulnerability class:

- Severity: WARNING → ERROR; subcategory: audit → vuln
- Confidence/likelihood/impact: LOW/LOW/MEDIUM → HIGH/MEDIUM/HIGH
- CWE-913 → CWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
- OWASP A01 (Broken Access Control) → A08 (Software and Data Integrity Failures)
- Drop actions/checkout requirement — any step with ref: is in scope
- Replace broad github.event.pull_request metavariable-pattern with precise
  metavariable-regex targeting head.sha, head.ref, github.head_ref, refs/pull/
- Extend regex to also cover refs/pull/ merge refs (existing test coverage)
- Absorb new test cases: github.head_ref, head.ref, sha||github.ref

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iable-regex

Keeps the original actions/checkout + jobs/steps scaffolding. Replaces the
broad generic-language metavariable-pattern on $EXPR with a metavariable-regex
that precisely targets the dangerous fork-head refs:
  github.event.pull_request.head.sha, github.event.pull_request.head.ref,
  github.head_ref, refs/pull/ merge refs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Semgrep reports matches at the `uses:` line, not the `ref:` line inside
`with:`. Move ruleid comments to precede `- uses:` to match TP-1/2/3 style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the previous approaches with the correct technique: keep the original
pattern structure (actions/checkout + jobs/steps scaffold), but improve the
metavariable-pattern on $EXPR to use pattern-either with a nested
metavariable-regex rather than a broad literal prefix match.

The generic-mode patterns match as substrings against the captured $EXPR value
(e.g. "${{ github.event.pull_request.head.sha }}"), so no ${{ }} wrapper is
needed in the sub-patterns:
  - github.event.pull_request.head.$PR_REF + regex ^(sha|ref)$ catches the
    two specific dangerous head fields while excluding .number, .body, etc.
  - github.head_ref ... catches the shorthand form

Removes the refs/pull/ test case — that pattern uses .number (not a head ref)
and was never covered by the new rule being merged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$...PR_REF captures the full multi-token expression between ${{ and }},
avoiding the single-token limitation of $PR_REF. metavariable-regex does
not support $...VAR, so a nested metavariable-pattern with pattern-either
is used instead to match the three dangerous fork-head ref forms:
  - github.event.pull_request.head.sha
  - github.event.pull_request.head.ref
  - github.head_ref

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses pattern-inside to scope matches to within ${{ }} expressions,
then pattern-either to match the specific dangerous fork-head refs.
No metavar capture needed — cleaner than the nested metavariable-pattern approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds github.event.pull_request ... back to the pattern-either so the
original broad coverage (including refs/pull/.../merge via .number) is
preserved alongside the explicit head.sha, head.ref, and head_ref patterns.
Restores the refs/pull/ spelling test case accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
head.sha and head.ref are already matched by the broader prefix pattern.
Test cases for both remain (TP-1/2/5 cover head.sha, TP-6 covers head.ref).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quality issues fixed:
- pattern-regex (raw text scan) -> pattern-inside + pattern + metavariable-pattern,
  matching the approach used by third-party-action-not-pinned-to-commit-sha and
  pull-request-target-code-checkout; comment exclusion handled by YAML parser
- Add pattern-inside: "{steps: ...}" scope (consistent with comparable rule)
- CWE-1104 (Unmaintained Component) -> CWE-1357 + CWE-353 (Integrity Check);
  CWE-353 is exact for "not verifying integrity of what you execute"
- Remove wrong reference to pwn-requests paper (unrelated topic)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread yaml/github-actions/security/gha-pwn-request-fork-checkout.test.yaml Outdated
@inkz inkz merged commit fc7be6a into develop Mar 31, 2026
10 checks passed
@inkz inkz deleted the feat/gha-supply-chain-rules branch March 31, 2026 21:37
0xDC0DE added a commit that referenced this pull request Apr 7, 2026
* Fix languages mixed

* Fix more

* fix more rules

* fix rules again

* Add GHA supply chain security rules (mutable-action-tag, pwn-request) (#3783)

* Add GHA supply chain security rules (mutable-action-tag, pwn-request)

github-actions-mutable-action-tag (WARNING / CWE-1104):
- Detects non-SHA-pinned uses: references via pattern-regex with negative
  lookahead for 40-char hex SHA. Catches all mutable refs including tagged
  versions, branches, and 'latest'. Validated against 853 peer-vendor repos
  (6,147 findings, 0 SHA-pinned false positives).
- Motivated by TeamPCP campaign where trivy-action@0.29.0 and
  kics-github-action@master were repointed to malicious commits.

gha-pwn-request-fork-checkout (ERROR / CWE-829):
- Detects pull_request_target + fork-controlled checkout ref combination
  (the "Pwn Request" attack pattern). Uses pattern-inside for trigger
  detection + metavariable-regex for fork ref forms.
- Confirmed TPs: sigstore/community (CRITICAL, PULUMI_ACCESS_TOKEN),
  jfrog/jfrog-cli (HIGH), SonarSource/official-images (HIGH).

Both ported from semgrep/semgrep-rules-jsonnet PRs #10484 and #10485
per reviewer feedback that community rules belong in this repository.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Merge gha-pwn-request-fork-checkout into pull-request-target-code-checkout

Absorbs the new rule's improvements into the existing rule rather than
shipping a duplicate rule for the same vulnerability class:

- Severity: WARNING → ERROR; subcategory: audit → vuln
- Confidence/likelihood/impact: LOW/LOW/MEDIUM → HIGH/MEDIUM/HIGH
- CWE-913 → CWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
- OWASP A01 (Broken Access Control) → A08 (Software and Data Integrity Failures)
- Drop actions/checkout requirement — any step with ref: is in scope
- Replace broad github.event.pull_request metavariable-pattern with precise
  metavariable-regex targeting head.sha, head.ref, github.head_ref, refs/pull/
- Extend regex to also cover refs/pull/ merge refs (existing test coverage)
- Absorb new test cases: github.head_ref, head.ref, sha||github.ref

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Restore original message text in pull-request-target-code-checkout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Restore original pattern structure; improve ref matching with metavariable-regex

Keeps the original actions/checkout + jobs/steps scaffolding. Replaces the
broad generic-language metavariable-pattern on $EXPR with a metavariable-regex
that precisely targets the dangerous fork-head refs:
  github.event.pull_request.head.sha, github.event.pull_request.head.ref,
  github.head_ref, refs/pull/ merge refs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix ruleid comment placement in TP-4/5/6 test cases

Semgrep reports matches at the `uses:` line, not the `ref:` line inside
`with:`. Move ruleid comments to precede `- uses:` to match TP-1/2/3 style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use metavariable-pattern + nested metavar-regex for precise ref matching

Replaces the previous approaches with the correct technique: keep the original
pattern structure (actions/checkout + jobs/steps scaffold), but improve the
metavariable-pattern on $EXPR to use pattern-either with a nested
metavariable-regex rather than a broad literal prefix match.

The generic-mode patterns match as substrings against the captured $EXPR value
(e.g. "${{ github.event.pull_request.head.sha }}"), so no ${{ }} wrapper is
needed in the sub-patterns:
  - github.event.pull_request.head.$PR_REF + regex ^(sha|ref)$ catches the
    two specific dangerous head fields while excluding .number, .body, etc.
  - github.head_ref ... catches the shorthand form

Removes the refs/pull/ test case — that pattern uses .number (not a head ref)
and was never covered by the new rule being merged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use $...PR_REF ellipsis capture with nested metavariable-pattern

$...PR_REF captures the full multi-token expression between ${{ and }},
avoiding the single-token limitation of $PR_REF. metavariable-regex does
not support $...VAR, so a nested metavariable-pattern with pattern-either
is used instead to match the three dangerous fork-head ref forms:
  - github.event.pull_request.head.sha
  - github.event.pull_request.head.ref
  - github.head_ref

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Simplify metavariable-pattern using pattern-inside ${{ ... }}

Uses pattern-inside to scope matches to within ${{ }} expressions,
then pattern-either to match the specific dangerous fork-head refs.
No metavar capture needed — cleaner than the nested metavariable-pattern approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Retain original github.event.pull_request ... pattern alongside new refs

Adds github.event.pull_request ... back to the pattern-either so the
original broad coverage (including refs/pull/.../merge via .number) is
preserved alongside the explicit head.sha, head.ref, and head_ref patterns.
Restores the refs/pull/ spelling test case accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove patterns subsumed by github.event.pull_request ...

head.sha and head.ref are already matched by the broader prefix pattern.
Test cases for both remain (TP-1/2/5 cover head.sha, TP-6 covers head.ref).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Rewrite github-actions-mutable-action-tag to use proper YAML patterns

Quality issues fixed:
- pattern-regex (raw text scan) -> pattern-inside + pattern + metavariable-pattern,
  matching the approach used by third-party-action-not-pinned-to-commit-sha and
  pull-request-target-code-checkout; comment exclusion handled by YAML parser
- Add pattern-inside: "{steps: ...}" scope (consistent with comparable rule)
- CWE-1104 (Unmaintained Component) -> CWE-1357 + CWE-353 (Integrity Check);
  CWE-353 is exact for "not verifying integrity of what you execute"
- Remove wrong reference to pwn-requests paper (unrelated topic)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(kotlin): exclude ephemeral port detection from unencrypted-socket rule (#3797)

ServerSocket(0) used to detect available ports (→ localPort → close())
is a common benign pattern that does not transmit cleartext data.
Add pattern-not-inside clauses scoped to functions returning Int.

Closes SRC-12442

Made-with: Cursor

* Add secrets-inherit rule for GitHub Actions workflows (#3803)

Detects use of `secrets: inherit` in reusable workflow calls, which
passes all repository secrets to the called workflow. This violates
least privilege — callers should explicitly pass only needed secrets.

---------

Co-authored-by: Katrina Liu <katrina@semgrep.com>
Co-authored-by: Kurt Boberg <98792107+kurt-r2c@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Pieter De Cremer (Semgrep) <pieter@r2c.dev>
Co-authored-by: Jonathan Roemer <jon@roemersoftworks.com>
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