On September 1, 2022, Snyk shipped CLI version 1.996.0 to close a command injection hole that had been sitting in its Go dependency scanner for an unknown period, and the CVE — CVE-2022-40764 — was publicly disclosed four weeks later, on September 29, 2022. The bug lived in snyk-go-plugin, the component the Snyk CLI invokes when it runs snyk test against a Go project, and it was found and reported by researchers at Imperva. The mechanism was almost mundane: a project's vendor.json manifest — a file an attacker fully controls if they can get a victim to scan their repository — has an ignore field, and the plugin fed that field into a command executed through a shell without neutralizing shell metacharacters. Rated CVSS 3.1 medium at 6.4, Imperva and Snyk both noted it was "hard to exploit" in typical CI, but far easier against IDE integrations that auto-scan a workspace the moment a file changes. Worse, the first patch was incomplete: a second advisory, tracked separately in the GitHub Advisory Database, showed the same class of flaw persisting across several Snyk plugins until version 1.1064.0. This piece walks through what actually went wrong and what a CLI tool has to do differently to avoid repeating it. (Note: the specific advisory URL supplied for this analysis, security.snyk.io/vuln/SNYK-CLI-COMMANDINJECTION-3149990, returned a 404 at the time of writing; the facts below are drawn from Snyk's own blog post, its support-portal advisory, and the GitHub Advisory Database entry for the follow-up fix.)
What was the actual root cause of CVE-2022-40764?
The root cause was string-built shell execution: snyk-go-plugin needed to run a Go toolchain command to inspect a project's vendored dependencies, and it assembled that command as a single shell string that incorporated the ignore field from the project's own vendor.json file. Because vendor.json ships inside the scanned repository, an attacker who controls the repository controls that string. If the field contained shell metacharacters — a semicolon, backticks, or a pipe — and the resulting string was handed to a shell interpreter (the classic exec("sh -c " + userInput) pattern) rather than passed as a discrete, non-shell-interpreted argument, whatever followed the metacharacter executed as an independent command. This is CWE-78, OS Command Injection, in its most textbook form: untrusted, structured input from a data file crossing into a command-execution sink without being tokenized or escaped first.
Why is a security scanner an unusually attractive target for this bug class?
Because the entire value proposition of snyk test is running it against code you don't fully trust yet — new dependencies, a contributor's pull request, a repository you're evaluating before adoption. A command injection bug in the scanner itself inverts that trust model: the artifact you scan to find out whether it's dangerous becomes the delivery mechanism for compromising your machine or CI runner. Imperva's write-up flagged IDE plugins as the sharper edge of this problem, since editors like VS Code and IntelliJ commonly auto-trigger a Snyk scan on file save or workspace open, with no separate user action to gate on — a developer opening an untrusted repository could trigger the injected command without ever intentionally running a scan. CI pipelines that run snyk test on every pull request from a fork face the same exposure: the PR author supplies the vendor manifest, and the scanning job runs with whatever credentials that CI stage holds.
Why did the first fix (1.996.0) turn out to be incomplete?
Because the September 2022 patch addressed the specific field and code path Imperva reported rather than the underlying pattern of building shell commands from file-derived strings. The GitHub Advisory Database's follow-up entry (GHSA-4x6g-3cmx-w76r) documents that snyk before 1.1064.0, along with snyk-mvn-plugin before 2.31.3, snyk-gradle-plugin before 3.24.5, snyk-docker-plugin before 5.6.5, snyk-python-plugin before 1.24.2, snyk-sbt-plugin before 2.16.2, and the CocoaPods and Hex plugins, all remained exploitable via crafted command-line flags passed to snyk test. This is a familiar failure mode in incident response: closing one instance of a vulnerable pattern without auditing every other call site that shares the same unsafe construction leaves siblings of the same bug live under a different trigger, in a different plugin, discovered later as a distinct advisory rather than folded into the original fix.
What is the correct fix for this class of bug, structurally?
The structural fix is eliminating the shell as an intermediary entirely: use an argument-array API — Node.js's execFile() or spawn() without the shell: true option, or Go's os/exec.Command with separate argument slots — so each value is passed to the target program as a discrete argv element rather than concatenated into a string a shell re-parses. When shell: true (or an equivalent sh -c wrapper) is never invoked, metacharacters like ;, `, $(), and | have no special meaning because there is no shell present to interpret them; the OS passes the string through to the target program's argv untouched. Where a shell genuinely is required — for glob expansion or piping — every untrusted value needs strict allow-list validation (reject anything outside an expected character set for a filename or version string) before it reaches the command line, since escaping functions are notoriously easy to get subtly wrong across platforms and shells.
How should teams defend against this class of risk in their own toolchains?
Treat every CLI tool, scanner, or build plugin that ingests repository-controlled content as untrusted input, not just the application code you're shipping. That means running dependency and vulnerability scanners with the same blast-radius thinking applied to production code: least-privilege service accounts, sandboxed or containerized execution for scans triggered by external pull requests, and prompt patching when a scanner vendor discloses a CVE in the scanner itself — the September 2022 disclosure is a reminder that the tools protecting your supply chain are themselves part of it. On the detection side, Safeguard's SCA engine matches every resolved dependency and plugin version against CVE and GHSA advisories, including exploited-in-the-wild signals from CISA KEV, so a lingering pre-1.996.0 or pre-1.1064.0 Snyk installation in a build image would surface as a known, patchable finding rather than a silent gap. Reachability-aware triage then helps separate scanner-tooling CVEs that sit on a path your CI actually executes from ones that don't, so the fix gets prioritized against real exposure instead of a bare version-string match.