On September 9, 2018, a trusted maintainer of the npm package event-stream added a dependency called flatmap-stream@0.1.1 that used the importing package's own npm description field as a decryption key to unpack and eval() an obfuscated payload in memory — code built specifically to steal private keys from the Copay bitcoin wallet's build pipeline. The package had already been pulled roughly 8 million times by the time researchers flagged it on November 20, 2018, more than two months later. That gap is the point: developer tooling — CLI utilities, build scripts, IDE plugins — runs with the same OS-level privileges as the developer who installed it, executes automatically on load or on every build, and is trusted implicitly because it sits inside the toolchain rather than in front of it. When that tooling passes untrusted strings — a config value, a repo file, a plugin manifest field — into eval(), vm.runInContext, Python's eval/pickle, or an unsandboxed template engine, the injection risk isn't hypothetical; it's a proven, repeated pattern spanning npm packages, VS Code extensions, and template-driven scaffolding tools. This piece walks through why developer tooling is a disproportionately attractive injection target, what recent research on the VS Code Marketplace found, and the secure-design patterns — sandboxed templating, source-to-sink tracing, least-privilege plugin execution — that actually close the gap.
Why is developer tooling a higher-value code-injection target than a typical web app?
Developer tooling is a higher-value target because a single compromised CLI package or IDE extension runs with full local OS privileges on every machine that installs it, with no browser sandbox, no same-origin policy, and often no confirmation prompt at all. A web app's injection surface is bounded by what the browser or server process can reach; a malicious npm postinstall script, a Python setup.py, or a VS Code extension's activation event runs the moment it's installed, before a human ever reviews the code. The event-stream incident exploited exactly this: flatmap-stream's payload only activated inside Copay's build environment, using the target package's own metadata as an evaluation key so static scanners looking for hardcoded malicious strings found nothing. That's the structural danger of eval()-based tooling — the dangerous string never appears in the source at rest, only at runtime, after decryption or template interpolation. A pattern-matching scanner that greps for eval( catches the naive case; it does not catch a payload assembled from environment data at execution time.
What did research on the VS Code Marketplace find about plugin-level injection risk?
Independent research from Koi Security, Wiz, ReversingLabs, and an academic study published on arXiv (2411.07479) examined the VS Code Marketplace during 2024 and 2025 and found the risk was systemic, not anecdotal. Koi Security's own analysis documented 1,283 extensions bundling known-malicious dependencies across a combined 229 million installs, 87 extensions reading /etc/passwd, 8,161 extensions contacting hardcoded IPs, and 1,452 executing unknown binaries or DLLs at runtime; as part of the same research, Koi published its own typosquatted copy of the popular "Dracula" theme, silently collecting host and system information over HTTPS, to demonstrate how easily a fake extension can pass marketplace review. Separately, Wiz found over 550 leaked marketplace and OpenVSX publisher tokens spread across more than 500 extensions — any single leaked token is enough for an attacker to push a malicious update straight to that extension's entire install base, no code review required. The common root cause across all of these: VS Code extensions activate automatically at IDE launch with the same privileges as the logged-in user, and there is no runtime sandbox separating "renders syntax highlighting" from "reads your SSH keys."
How does unsafe templating turn into remote code execution, and why does it keep recurring?
Unsafe templating turns into remote code execution when a CLI scaffolding tool, doc generator, or plugin config renderer feeds user- or repo-controlled input into a full-featured template engine that supports arbitrary expression evaluation, rather than a logic-less one. This is the same root cause as server-side template injection (SSTI) in web frameworks — engines like Jinja2 or Handlebars-style templates that allow attribute access and method calls inside {{ }} blocks will happily execute an attacker-supplied expression if the template string itself is untrusted, not just the variables filled into it. In a CLI context this shows up when a tool renders a project's own config file, README front matter, or a plugin manifest as a template: if the file content — not just data values — comes from a cloned repository or a downloaded package, an attacker only needs to get a crafted string into that file to reach code execution the moment a developer runs the tool locally. The fix pattern is well established and narrow: never render untrusted strings as executable template syntax, and where dynamic content is required, use a logic-less templating mode that restricts output to variable substitution with no method calls or attribute traversal.
What does secure design actually look like for CLI and plugin authors?
Secure design for CLI tools and IDE plugins rests on three practices: eliminate eval()-class calls on any input that can trace back to a file, argument, or network response you don't control; adopt least-privilege execution so a plugin's declared capabilities (network, filesystem, process spawn) are enforced rather than assumed; and treat the plugin manifest and its dependency tree as untrusted input requiring the same install-time vetting as a production dependency. In practice this means CLI authors should prefer subprocess APIs with explicit argument arrays over shell string interpolation, use sandboxed template engines that disable method/attribute access by default, and pin and verify plugin dependencies rather than resolving them fresh on every install — the event-stream attack succeeded in part because flatmap-stream was a new, unpinned transitive dependency that nobody had reason to re-review. IDE vendors are also moving toward capability-scoped extension APIs, but as of 2026 most desktop IDE ecosystems still activate third-party extension code with the full privileges of the host process, which means the burden of least-privilege currently falls on the plugin author and the org vetting what gets installed.
How Safeguard Helps
Safeguard's SAST engine traces exactly this pattern before it ships: it follows untrusted data from a source — a CLI argument, a config file read, a repo-controlled string — through the call graph to a dangerous sink like eval(), vm.runInContext, or an unsandboxed template render, and returns the full source→sink dataflow trace with a CWE mapping rather than a bare "eval() found" line-number hit. That's the difference between flagging every eval() call in a codebase and flagging the two that an attacker-controlled repo file could actually reach. On the supply-chain side, Safeguard's Package Firewall sits as an install-time proxy in front of npm and pip, evaluating every fetch — including transitive dependencies like flatmap-stream would have been — for typosquatting, namespace confusion, and known-malicious signatures before the package ever lands on a developer's machine or a build runner. Together, the two engines cover both halves of the CLI/plugin injection problem: the unsafe eval-and-template code your team writes, and the malicious dependency someone else might quietly add to your build.