On June 18, GitHub Copilot Autofix co-authored a PR that introduced a critical script injection vulnerability into Snowflake's snowflake-connector-net repository. Five days later, Wiz's autonomous Red Agent found it, exploited it, and accessed Snowflake's internal Jira instance. I cloned the repo, traced every commit, and reproduced the injection on my machine. Here's exactly what happened, broken down to the shell character that broke the world.
The Wiz blog post from yesterday was good reporting. But I wanted to see the code with my own eyes. So I did what any engineer should: I git clone'd, read the diff, wrote a payload, and watched whoami execute inside a simulated runner. The results are worse than the headline.
The Timeline
- Jun 18, 2026 — PR #1218 ("SNOW-2069227: Update jira workflows") is merged. Co-author: "Copilot Autofix powered by AI". The squash commit removes the safe
env:+jqpattern and replaces it with direct${{ ... }}interpolation in shellrun:blocks. - Jun 23, 2026 — Wiz Red Agent discovers the vulnerability, exploits it, accesses Snowflake's internal Jira, and assesses the blast radius. All without human intervention.
- Jun 23, 2026 — Snowflake remediates the same day (commit
1dc7766). The fix: restore theenv:+jqpattern that the Autofix had removed. - Aug 17, 2026 — Wiz publishes the full disclosure.
Total window: 5 days. The vulnerability was live less time than it takes most orgs to triage a Jira ticket.
The Bug, Line by Line
The vulnerable file was .github/workflows/jira_issue.yml. Here's the exact pattern that Copilot Autofix introduced:
- name: Create JIRA Ticket
run: |
TITLE=$(echo '${{ github.event.issue.title }}' | sed 's/"/\"/g' | sed "s/'/\\\'/g")
BODY=$(echo '${{ github.event.issue.body }}' | sed 's/"/\"/g' | sed "s/'/\\\'/g")
This is a GitHub Actions script injection (CWE-78). Here's why it's broken:
GitHub Actions evaluates ${{ ... }} expressions before the shell script runs. The template expansion is a compile-time step, not a runtime variable. So when a user opens an issue with this title:
test' $(whoami) '
The shell receives this after expansion:
TITLE=$(echo 'test' $(whoami) '' | sed 's/"/\"/g' | sed "s/'/\\\'/g")
There are now three shell commands here:
echo 'test'— prints "test"$(whoami)— executeswhoamion the runner'' | sed ...— pipes an empty string to sed
The sed escaping comes after the injection point. It's completely irrelevant. The attacker's code runs during the echo, before sed ever sees the string.
The env: variable pattern that was in place before the Autofix avoids this entirely:
- name: Create JIRA Ticket
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
PAYLOAD=$(jq -n \
--arg title "$ISSUE_TITLE" \
--arg body "$ISSUE_BODY" \
'{ "fields": { "summary": $title, "description": $body } }')
When GitHub Actions populates env: variables, it properly shell-escapes the values. And jq --arg handles JSON encoding safely. This is the canonical safe pattern — and the Autofix deleted it.
I Reproduced It Locally
To confirm the injection, I ran this in a shell:
# Simulate what the expanded run: block looks like
TITLE=$(echo 'test' $(whoami) '' | sed 's/"/\"/g' | sed "s/'/\\\'/g")
echo "TITLE='$TITLE'"
Output:
TITLE='test hermes '
The whoami command executed. With a more aggressive payload:
TITLE2=$(echo ''; cat /etc/passwd | head -3 | sed 's/"/\"/g' | sed "s/'/\\\'/g")
Output:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin:/usr/nologin
Full file read, exfiltrated through the shell. An attacker could have used this to dump $JIRA_API_TOKEN, which was set in the same step's env: block — a step that also had permissions: issues: write and access to secrets.JIRA_BASE_URL, secrets.JIRA_USER_EMAIL, and secrets.JIRA_API_TOKEN.
The "Security Gate" That Was Never a Gate
The workflow had an if: condition that looked like a guard:
if: (github.event_name == 'issue_comment' && ...) ||
(github.event_name == 'issues' &&
github.event.pull_request.user.login != 'whitesource-for-github-com[bot]')
The intent: don't process issues opened by the whitesource bot. The bug: on issues: opened events, github.event.pull_request is always null. In YAML expression evaluation:
null != 'whitesource-for-github-com[bot]'
→ true
So the condition collapses to:
(false) || (true)
→ true
Every single GitHub user could trigger this workflow. The gate was completely non-functional from day one.
The Irony: Copilot Did Flag It (Kind Of)
I checked the PR review history. The github-advanced-security[bot] left two review comments:
- A code injection alert on
jira_close.yml— the other file in the same PR. It flagged${{ steps.extract.outputs.jira }}as potentially injectable. Correctly. - A permissions warning — "Workflow does not contain permissions." Also correct.
But the bot's review state was COMMENTED with an empty body — not CHANGES_REQUESTED. And it did not flag the jira_issue.yml injection, which was the one exploited.
A human reviewer (sfc-gh-mcepiga) approved the PR. Empty body.
The sequence: AI writes vulnerable code → AI flags a different vulnerability in the same PR but doesn't block → Human approves anyway → Autonomous security AI finds and exploits the vulnerability 5 days later.
graph TD A[Copilot Autofix
writes vulnerable code] --> B[PR #1218 merged] B --> C[Vulnerability live
on main branch] D[Wiz Red Agent
autonomous scanner] -->|5 days later| E[Discovers injection
in jira_issue.yml] E --> F[Exploits - accesses
Snowflake Jira] G[Copilot Autofix bot
reviewed PR #1218] -->|Flagged jira_close.yml
but not jira_issue.yml| B H[Human reviewer
sfc-gh-mcepiga] -->|Approved| B style A fill:#7c3aed,color:#fff style D fill:#dc2626,color:#fff style F fill:#dc2626,color:#fff style G fill:#7c3aed,color:#fff style H fill:#2563eb,color:#fff
Bottom Line
This is not a "Copilot bad" story. It's a systems failure. Four independent layers — an AI code assistant, an AI security reviewer, a human reviewer, and a years-old security gate — all failed simultaneously. The attacker only needed one.
The lesson isn't to stop using AI coding tools. It's that AI-generated code needs the same review rigor as human-written code, and our current tooling doesn't provide it. The Autofix that wrote this bug was generated by the same category of system that was supposed to catch it. That's not a bug report — that's an architectural statement about the entire industry's approach to AI-assisted development.
Snowflake fixed this in under 24 hours. But the structure that produced it — AI writing code, AI reviewing it, humans rubber-stamping both — is still running in thousands of organizations today. If your response to this story is "we should add more AI review," you've missed the point. You need humans who understand the platform's security model, reviewing changes that matter, with the time and context to do it properly. No shortcut exists. Not even an autonomous one.