An unauthenticated GitHub user could have run commands in a Snowflake GitHub Actions workflow and extracted credentials for the company’s internal Jira. The flaw was live for five days in June before researchers used it to retrieve a Jira API token, demonstrating how a small change in a public automation file can cross into private engineering systems.
Wiz Research says its autonomous security tool, Red Agent, found the injection, developed a working exploit and reported it through Snowflake’s HackerOne program on June 23. Snowflake patched the workflow that day and rotated the token the next day. The company said its investigation found no evidence of unauthorized access, while Wiz said audit logs attributed all activity during the exposure window to its tests.
The disclosure is also a useful correction to a tempting AI-security headline. The pull request included a Copilot Autofix contribution, and GitHub’s automated security review missed the dangerous code. But Wiz updated its report to say it is unclear whether the vulnerable change itself was AI-assisted. The public commit history supports that narrower account: Copilot’s documented contribution affected a different workflow in the same pull request.
The shell met an untrusted issue title
The vulnerable file, jira_issue.yml, lived in Snowflake’s public .NET connector repository. Its job was to create an internal Jira ticket when someone opened a GitHub issue. To do that, the workflow had access to the Jira base URL, a user email and an API token stored as GitHub Actions secrets.
That is a common and reasonable automation pattern. The dangerous part was how the workflow moved public input into its shell script. A change in pull request #1218 removed the existing intermediate variables and JSON construction using jq. It instead placed GitHub expressions for the issue title and body directly inside a Bash run block. The key line was effectively:
run: |
TITLE=$(echo '${{ github.event.issue.title }}' | sed ...)
GitHub expands ${{ github.event.issue.title }} before the generated script reaches Bash. A single quote in a malicious issue title could therefore close the quoted string, add a command and turn the rest back into harmless shell text. The later sed commands did not provide protection because the attacker-controlled value had already become part of the script’s syntax.
This is not an obscure interpretation of Actions behavior. GitHub’s own documentation on script injection names github.event.issue.title as untrusted input and explains that expressions in an inline script are substituted before execution. Its recommended pattern is to assign the expression to an intermediate environment variable, then read that variable from the shell. That keeps the value as data rather than using it to generate code.
The Snowflake workflow had used that safer separation before the pull request. Its June 23 remediation commit restored ISSUE_TITLE, ISSUE_BODY and ISSUE_URL as environment variables. It also rebuilt the Jira request with jq -n --arg, allowing the JSON encoder to handle quotes and control characters instead of assembling JSON through shell interpolation.
A security condition that admitted everyone
A second mistake made the injection reachable. The job’s if expression appeared to exclude a particular bot when an issue event arrived, but it checked github.event.pull_request.user.login. An ordinary issue event has no pull_request object, so that value was null. The comparison against the bot name was consequently true, according to Wiz’s analysis.
The workflow listened for newly opened issues. Combined with the ineffective condition, that meant any GitHub account able to file an issue could start a job containing the Jira secrets. The attacker did not need commit access, an approved pull request or a compromised maintainer account. A title was enough to place commands on the runner.
This is the broader CI/CD lesson. The trust boundary of a workflow is determined by both its trigger and the resources available to its jobs. Issue titles, pull-request bodies, branch names and comments may look like repository metadata, but on a public project they are internet input. A workflow that consumes them while holding credentials is a small privileged application and needs to be reviewed like one.
From runner execution to internal Jira
Wiz says Red Agent first tried a payload that failed with a Bash syntax error. The agent then adjusted the shell construction, received an out-of-band callback from the GitHub-hosted runner and extracted the Jira URL, email and API token. This account comes from Wiz, which built the product being demonstrated; no independent reproduction is described in the disclosure. The public workflow diff does, however, show the underlying injection pattern and the secrets available to that step.
According to Wiz, the token authenticated to Snowflake’s Jira and provided read access across engineering, security-compliance and bug-bounty projects. That moved the incident beyond a theoretical command-injection finding. The proof of concept crossed a boundary from public GitHub input into a private system containing potentially sensitive operational records. Wiz said it deleted the data accessed during testing.
The scope was limited in important ways. The researchers did not report access to Snowflake customer data, production databases or Snowflake accounts. Snowflake’s statement said the vulnerability was in one public GitHub repository and that its investigation found no unauthorized access. Wiz said Snowflake’s audit logs matched anomalous Jira activity to the research team’s testing addresses.
The chronology also matters. The pull request was merged on June 18, 2026. Wiz reported the flaw on June 23, Snowflake restored the safer workflow the same day, and the affected credential was rotated on June 24. A five-day exposure is short compared with many disclosed vulnerabilities, but the report’s central claim is that automated discovery compressed that window further: its agent found and exercised the path before a human report arrived.
What Copilot did, and what it did not
The original framing around the disclosure risked collapsing several different facts into “AI wrote an insecure patch.” The record is more complicated.
The public pull request contains a commit whose message credits “Copilot Autofix powered by AI” as a co-author. That commit addressed jira_close.yml, a workflow for closing Jira issues. The vulnerable direct interpolation appeared later in a separate commit for jira_issue.yml, with no Copilot co-author line. Wiz’s updated disclosure explicitly says the vulnerable code change may or may not have been AI-assisted.
Copilot and automated security checks are still relevant, just in a different way. GitHub Advanced Security commented on the pull request and detected issues in jira_close.yml, including a separate potential injection that prompted the Copilot Autofix contribution. A later scan warned that the same file lacked an explicit permissions block. Yet it did not flag the direct issue-title interpolation in jira_issue.yml, even though that file was part of the final revision. A human reviewer then approved the pull request shortly before it was merged.
That sequence is more instructive than a simple story about faulty generated code. Automated review found one risk, helped produce one fix and missed a different critical risk elsewhere in the change set. Human review also missed it. The failure was not that a machine touched the code; it was that the combined review process treated tool output as incomplete evidence without catching the security regression.
Attribution matters because teams need the right remedy. If the lesson is merely “AI writes bad code,” an organization might add an AI label or another approval checkbox. Neither would have stopped a human from writing the same interpolation. The useful controls are structural: keep untrusted expressions out of shell source, restrict which triggers can reach secrets, give credentials the smallest possible scope, and test workflows for malicious metadata.
A practical review checklist for Actions
Repository owners can search workflow files for expressions inside run: blocks, especially fields ending in title, body, name, ref or message. A match is not automatically exploitable, but it should prompt a trace from the event trigger to the shell and then to any available token, network endpoint or artifact.
The safest repair is usually simple. Pass the expression through env: or an action input, quote the shell variable, and use a purpose-built encoder such as jq --arg for JSON. Avoid home-grown chains of sed substitutions for security boundaries. Escaping rules vary between YAML, GitHub’s expression engine, the shell and the destination format; trying to make one string safe across all four layers is brittle.
Teams should also review conditions against the event schema rather than assuming a field exists. A check that refers to a pull-request property during an issue event may fail open, as this one did. Tests should cover public actors and deliberately hostile titles and bodies, not only the expected maintainer path.
Finally, secrets should be short-lived and narrowly scoped. GitHub’s secure-use guidance recommends least privilege, periodic rotation and review gates for environment secrets. Those measures do not remove an injection, but they can keep a runner compromise from becoming broad, durable access to an internal system.
What to watch next is whether GitHub’s workflow analysis begins catching this exact regression reliably and whether repository owners audit similar issue-to-Jira automations. The Snowflake patch is public and the token is rotated; the unresolved question is how many other workflows still turn community-supplied metadata into privileged shell code while automated checks report no blocker.