Skip to content

@npmcli/promise-spawn — Windows cmd.exe Command Injection via Newline Characters #155

Description

@dfzysmy2tf-create

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

The cmd() escape function in lib/escape.js of the @npmcli/promise-spawn package fails to handle carriage return (\r) and newline (\n) characters when escaping arguments for Windows cmd.exe. While the function correctly prefixes shell metacharacters (e.g., !%^&()<>|") with ^ to prevent injection, it does not sanitize \r\n sequences. In Windows cmd.exe, \r\n is interpreted as a command separator, allowing an attacker to inject and execute arbitrary commands if they control the input passed to this function.
For example, if a malicious input like legitimate\r\necho PWNED is passed to the cmd() function, the escaped output will retain the \r\n characters. When executed via cmd.exe, this results in two separate commands being run instead of one.

Expected Behavior

The cmd() escape function should properly sanitize or escape all characters that can act as command separators in Windows cmd.exe, including \r and \n. Any input containing newline/carriage return characters should have these characters removed or escaped to prevent them from being interpreted as command separators. This ensures that even if untrusted input is passed to the function, it cannot be used to inject arbitrary commands into the shell.

Steps To Reproduce

  1. Environment Setup: Use a Windows system and install @npmcli/promise-spawn (tested on the latest stable version).
  2. Craft Malicious Input: Prepare a parameter containing \r\n (e.g., test\r\necho INJECTED_COMMAND).
  3. Invoke the cmd() Escape Function: Pass the malicious input to the cmd() function in lib/escape.js.
  4. Execute via cmd.exe: Use the escaped output to construct a command and execute it with cmd.exe (e.g., via the spawn method with shell: true).
  5. Observe Result: The cmd.exe will execute the original command and the injected command (e.g., echo INJECTED_COMMAND) separately, confirming the command injection vulnerability.
    Minimal Reproducible Code:
const { cmd } = require('@npmcli/promise-spawn/lib/escape.js');
const { spawn } = require('child_process');

// Malicious input with newline characters
const maliciousInput = 'legitimate\r\necho PWNED';
// Escape the input using the vulnerable cmd() function
const escapedInput = cmd(maliciousInput);
// Execute via cmd.exe on Windows
spawn(`echo ${escapedInput}`, { shell: true, stdio: 'inherit' });

Environment

  • npm: >=8.0.0
  • Node: >=14.0.0
  • OS: Windows
  • platform: x64

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bugthing that needs fixingNeeds Triageneeds an initial review

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions