Introduction
A helper function designed to stream subprocess output in an Electron application accepted arbitrary command strings without validation, creating a latent command injection vector. The runStreaming() utility wraps child_process to handle long-running Python package installations (specifically Paddle wheels and model archives), but its signature—runStreaming(cmd, args, opts)—trusted callers to provide safe inputs.
The problem: nothing in runStreaming() enforced that trust. A caller receiving user-influenced data could pass a malicious cmd like "; rm -rf / #" or an args array containing shell metacharacters. While the function used the array-form child_process API (avoiding shell: true), an attacker controlling both cmd and args could still exploit path traversal or argument injection depending on the target binary.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — see commit with SAFE_CMD regex addition |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-1357 (Reliance on Insufficiently Trustworthy Component) |
The Vulnerability Explained
The vulnerable code accepted cmd as a string and args as an array without any inspection:
const runStreaming = (cmd, args, opts = {}) => new Promise((resolve, reject) => {
if (quitting) { reject(new Error('应用正在退出')); return }
let proc
// ... spawns process with cmd and args directly
An attacker who influenced cmd could pass a relative path containing directory traversal (../../../malicious) or a binary name with shell metacharacters. More critically, if any caller concatenated user input into cmd before passing it, the lack of validation meant that injection propagated directly to process execution.
The real-world impact depends on data flows upstream of runStreaming(). The function handles package downloads for machine learning workflows—contexts where mirror URLs, proxy configurations, or user-supplied index URLs might reach this code path. An attacker compromising a package mirror or supplying a malicious index-url could potentially influence the command executed.
The Fix
The patch adds defense-in-depth validation that fails closed:
const SAFE_CMD = /^[A-Za-z0-9_.: \\/-]+$/
if (typeof cmd !== 'string' || !SAFE_CMD.test(cmd) || !Array.isArray(args) || args.some((a) => typeof a !== 'string')) {
reject(new Error('非法的子进程调用参数'))
return
}
This change introduces three specific protections:
-
Type enforcement:
cmdmust be a string,argsmust be an array, and every element ofargsmust be a string. This blocks prototype pollution attacks and type confusion bugs. -
Character whitelist: The
SAFE_CMDregex permits only alphanumeric characters, safe path separators, and limited punctuation. It explicitly rejects semicolons (;), pipes (|), backticks (`), dollar signs ($), and other shell metacharacters. -
Explicit failure: Invalid inputs reject with a clear error message rather than proceeding with potentially dangerous data.
The fix maintains the existing array-form invocation (no shell), so the regex is strictly defense-in-depth—it closes off an entire class of vulnerabilities that would emerge if a caller ever passed unsanitized input.
Key Takeaways
-
Whitelist over blacklist: The
SAFE_CMDregex defines permitted characters rather than trying to enumerate dangerous ones, avoiding the inevitable gaps in blacklist approaches. -
Validate at trust boundaries:
runStreaming()is a trust boundary between application logic and the operating system. Validation belongs here even if all current callers appear safe. -
Array-form spawning isn't enough: While
child_process.spawn(cmd, args)withoutshell: trueprevents direct shell injection, a maliciouscmdcan still target unexpected binaries or exploit argument injection in the target program. -
Type safety is security: The
typeofchecks catch JavaScript's dynamic typing footguns—argscould be an object with a numericlengthproperty, orcmdcould be a String object with dangerous prototype methods. -
Defense-in-depth justifies unexploitable fixes: Even without a demonstrated exploit, bounding failure modes prevents vulnerabilities from emerging as code evolves.
How Orbis AppSec Detected This
Source: The cmd parameter of runStreaming(), potentially influenced by upstream configuration values like mirror URLs or user settings.
Sink: child_process invocation within runStreaming(), where cmd and args are passed to process spawning.
Missing control: No validation of cmd contents, no type checking for args elements, and no restriction on shell metacharacters in command paths.
CWE: CWE-1357 (Reliance on Insufficiently Trustworthy Component) — the code relied on callers to provide trustworthy inputs without enforcing that contract.
Fix: Added whitelist regex and explicit type guards to reject any command string containing dangerous characters or any malformed argument array.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The runStreaming() fix demonstrates how defense-in-depth validation transforms a latent vulnerability into an explicit, bounded failure mode. By whitelisting safe command characters and enforcing type contracts, the code now fails securely even against hypothetical attacker-controlled inputs—protecting against supply chain compromises and configuration injection attacks that might emerge in future code changes.