sakutto
Generative AI

GitHub Actions Script Injection: The Snowflake Bug

SecurityGitHub ActionsCopilot
GitHub Actions Script Injection: The Snowflake Bug

What a script injection in GitHub Actions is

The class has nothing to do with Snowflake, and it predates AI in the toolchain. It comes from one habit: putting text that a stranger controls into a place where the runner expects code.

The two halves of the bug

Untrusted input
Issue titles, PR bodies, branch names — anything a person outside the repository can set
Executable context
A run: block, where the value is pasted into the script before the shell parses it
Result
The value is read as commands, with whatever permissions and secrets the job holds
Safe form
Pass the value in through env:, so the shell reads a variable name instead of attacker text

Text becomes a command inside the run block

GitHub Actions expands the expression into the workflow file before the shell ever sees the line. By the time the shell parses it, the attacker's characters are already part of the script. Quoting the value inside the run: block does not help, because the substitution has already happened.

GitHub's documentation puts the rule in terms of flow: an untrusted value should never reach a place where it could be read as code. Escaping is the instinct. Moving the value out of that position is the fix.

View official source →
"Before the shell script is run, the expressions inside ${{ }} are evaluated and then substituted with the resulting values, which can make it vulnerable to shell command injection."(Understanding the risk of script injections)/"When your workflow runs, those strings might be interpreted as code which is then executed on the runner."(Understanding the risk of script injections)/"You should ensure that these values do not flow directly into workflows, actions, API calls, or anywhere else where they could be interpreted as executable code."(Understanding the risk of script injections) — GitHub Docs

Who is allowed to reach the run block

A workflow that fires on issues: opened has no membership requirement at all. Anyone with a GitHub account can open an issue on a public repository, so the trigger sets the audience for everything the job can do.

That gap decides whether a finding is theoretical or live. The same unsafe line is a minor issue under a trigger only maintainers can fire. Under issues: opened on a public repo, the audience is the whole internet.

View official source →
"The workflow triggered on issues: opened - meaning any GitHub user could fire it by opening an issue - and interpolated the attacker-controlled issue title directly into a shell script" — The Code Change, Wiz blog

How the Snowflake workflow gave up a Jira token

The repository was snowflakedb/snowflake-connector-net, and the workflow filed a Jira ticket whenever an issue was opened. Earlier versions of it were safe.

Before and after the change

Original
Issue title passed through an env: variable, JSON payload built with jq
After PR #1218
Issue title expanded directly into the shell script
Live window
18 June 2026 (merge) to 23 June 2026 (report)
Reached
A Jira API token with read access across engineering, security compliance and bug bounty projects
View official source →
"The exfiltrated token authenticated as qa@snowflake.net to snowflakecomputing.atlassian.net, granting read access across Snowflake's engineering, security compliance, and bug bounty tracking projects." — Exploitation, Wiz blog

The safe pattern that PR #1218 deleted

The env-variable-plus-jq construction exists for exactly one reason: it keeps untrusted text out of the shell. PR #1218 swapped it for direct interpolation. The unsafe version replaced a control that was already working.

That is the part that should unsettle you more than the exploit does. Nobody forgot anything here. A control got taken out, and nothing in the pipeline read its removal as a regression.

View official source →
"It removed the repository's existing safe pattern, which passed the issue title through an env: variable and built the JSON payload with jq. Instead it used the direct ${{ github.event.issue.title }} interpolation shown above." — Wiz blog

Was the bot check ever a gate?

No. The workflow compared a pull request field against a bot account name, and on issue events that field is null, so the comparison passed for everyone. A condition that reads like an access control can evaluate to true for every account on GitHub.

A reviewer skimming the file sees a check and moves on. This is the failure mode to look for: a condition that reads as protective, attached to an event it was never written for.

View official source →
"However, on issues events, github.event.pull_request is always null. So the condition reduces to (null != 'whitesource-for-github-com[bot]'). This is always true, and every GitHub user passes the gate." — Wiz blog

Where Copilot actually sits in this story

The headline version has an AI writing the bug and another AI finding it. What the sources actually support is narrower, and the difference matters if you plan to repeat the story.

What is established and what is not

Established
Copilot was recorded as a co-author on the squashed merge commit
Established
It reviewed the merged PR and the code change and returned all-clear
Not established
Whether the code change itself was AI-assisted

The confirmed failure is the review, not the authorship

An automated security review looked at this change and returned all-clear. Wiz revised its own post on 17 August to draw that line. The revision is the more useful claim anyway: a review that passes a critical injection is a repeatable failure, whoever typed the line.

Treating the authorship question as settled would go past the evidence. Wiz states plainly that it is unclear whether the change was AI-assisted, and no source establishes otherwise.

View official source →
"August 17, 2026, 1957 UTC update: This blog has been updated to clarify that Copilot was a co-author that checked the merged PR and code change, and identified it as all-clear without noticing the critical vulnerabilities. It's unclear whether the code-change was AI-assisted." — Wiz blog

Why generated changes drop safe patterns

A model predicts code from probabilistic patterns, and Wiz notes those patterns can reintroduce insecure shell constructions. The safe pattern is the more verbose form, so it is the one more likely to get smoothed away.

The removal then survives review because the reasoning is missing. Wiz makes the point directly: automated assistants lack the historical context for why a particular pattern was chosen, so a construction built to stop shell injection reads as a roundabout way to build a string.

View official source →
"Automated AI assistants often lack historical context regarding why specific code patterns were chosen. In this incident, an automated PR removed a safe env: + jq parsing pattern that had been explicitly implemented to prevent shell injection." — Wiz blog

Closing this in your own workflows

The first two changes cover most of the exposure, and neither needs new tooling.

What to change, in order

1
Grep every workflow for expressions inside run: blocks, and move each one to an env: variable
2
Re-read every if: condition against the events that actually trigger the job, not the event you had in mind
3
Put generated PRs through the same static analysis as human ones, with no fast path
4
Scope and shorten workflow credentials, so a runner compromise reaches less and expires sooner

Give generated PRs no shorter path than human ones

Scrutiny should attach to the change, whoever or whatever produced it. A generated PR that removes a control is the same regression as a human one, and a pipeline that trusts the author label will keep waving them through.

Guardrails cover what review misses. The specific move here — swapping a structured parser for direct string building — is mechanically detectable, and a linter does not need to understand the code to fire on it.

View official source →
"AI coding tools predict code based on probabilistic patterns, which can inadvertently reintroduce deprecated or insecure shell patterns. AI-generated PRs must undergo the same static analysis and security scrutiny as human code." — Wiz blog

Assume discovery in days

Five days passed between the merge and a working exploit. The finder was an autonomous agent, and Wiz frames the lesson as a shift in tempo: discovery now happens in hours, which is what patch cycles and credential lifetimes have to be measured against.

Copy Snowflake's response, not its workflow: the fix shipped the day it was reported, and the credential was rotated the day after.

View official source →
"The vulnerability was live for only five days before an automated agent discovered and validated it. Security operations must adapt to a landscape where automated discovery occurs in hours, requiring rapid patch cycles and short-lived credentials." — Key Takeaways, Wiz blog

Model 2: the model Anthropic has not released covers what happens when AI systems get audited instead of attacked. For the provenance side of the same trust question, see Claude Text Watermark: What a Hit Actually Proves.

Reading a vendor advisory alongside the commit it describes is easier once both are plain text.

Free ToolURL to Markdown ConverterConvert any public web page URL to Markdown. Preserves headings, tables, lists, and links — perfect for LLM and RAG preprocessing, research notes, and archiving web articles.Try it now →

FAQ

Q. What makes an issue title dangerous in a workflow?
Nothing about the title itself. The danger is where it lands. GitHub's documentation is explicit that untrusted strings dropped into a run block can be read as code rather than as data, and an issue title is attacker-controlled by definition, because anyone with an account can open an issue on a public repository.
GitHub Docs — Script injections
When your workflow runs, those strings might be interpreted as code which is then executed on the runner. GitHub Docs — Script injections
Q. Did Copilot write the vulnerable code?
That is not established. Wiz updated its post on 17 August to say Copilot was a co-author that checked the merged PR and marked it all-clear, and that whether the code change itself was AI-assisted is unclear. The confirmed failure is a review that passed a critical bug, not authorship.
Wiz Blog — Wiz Red Agent Finds Its Way Into Snowflake's Internal Jira
August 17, 2026, 1957 UTC update: This blog has been updated to clarify that Copilot was a co-author that checked the merged PR and code change, and identified it as all-clear without noticing the critical vulnerabilities. It's unclear whether the code-change was AI-assisted. Wiz Blog — Wiz Red Agent Finds Its Way Into Snowflake's Internal Jira
Q. How long do I have to patch something like this?
Less time than the old assumptions allow. This flaw was live for five days before an autonomous agent found it, exploited it and reported it. Planning around a scanning cadence measured in weeks no longer matches the discovery speed on the other side.
Wiz Blog — Wiz Red Agent Finds Its Way Into Snowflake's Internal Jira
The vulnerability was live for only five days before an automated agent discovered and validated it. Wiz Blog — Wiz Red Agent Finds Its Way Into Snowflake's Internal Jira

Related Tools

Related Tool Categories

Articles