Back to Blog
critical SEVERITY9 min read

How shell metacharacter injection happens in Node.js SSH provisioning and how to fix it

A critical command injection vulnerability was discovered in `services/onuProvisionService.js`, where the `sanitizeCliInput` function only stripped newlines and carriage returns from user input before passing it to SSH shell streams. Attackers could inject shell metacharacters like semicolons, pipes, and backticks to execute arbitrary commands on network infrastructure devices such as ONU/ONT hardware. The fix expands the sanitization regex to strip all dangerous shell metacharacters, closing th

O
By Orbis AppSec
Published August 1, 2026Reviewed August 1, 2026

Answer Summary

This is a shell command injection vulnerability (CWE-78) in Node.js, found in the `sanitizeCliInput()` function inside `services/onuProvisionService.js`. The function only removed newlines (`\r\n`) from user-supplied input before writing it to an SSH shell stream used to provision network devices. Attackers could inject shell metacharacters like `;`, `|`, `` ` ``, `&`, or `$()` to execute arbitrary commands on ONU/ONT hardware. The fix replaces the incomplete regex with one that strips all dangerous shell metacharacters: `/[\r\n;|&`$()<>!{}\\]+/g`.

Vulnerability at a Glance

cweCWE-78
fixExpanded the sanitization regex to remove all shell metacharacters before writing to SSH streams
riskRemote attackers can execute arbitrary commands on network infrastructure devices via SSH
languageJavaScript (Node.js)
root causesanitizeCliInput() only filtered \r\n characters, leaving shell metacharacters (; | & ` $ etc.) intact
vulnerabilityShell Command Injection via Incomplete Input Sanitization

How Shell Metacharacter Injection Happens in Node.js SSH Provisioning and How to Fix It

Summary

A critical command injection vulnerability was discovered in services/onuProvisionService.js, where the sanitizeCliInput function only stripped newlines and carriage returns from user input before passing it to SSH shell streams targeting ONU/ONT network devices. Attackers could inject shell metacharacters like semicolons, pipes, and backticks to execute arbitrary commands on network infrastructure. The fix expands the sanitization regex to remove all dangerous shell metacharacters, closing the injection path entirely.


Direct Answer: This is a shell command injection vulnerability (CWE-78) in Node.js. The sanitizeCliInput() function in services/onuProvisionService.js only removed newlines from user input before writing to an SSH shell stream. The fix replaces the incomplete regex /[\r\n]+/g with /[\r\n;|&\$()<>!{}\]+/g` to strip all dangerous shell metacharacters.


Introduction

The services/onuProvisionService.js file handles provisioning of ONU (Optical Network Unit) devices — the hardware endpoints in fiber-optic networks made by vendors like ZTE and Huawei. Functions like zteProvisionONU and huaweiProvisionONU accept parameters such as name, pon, and onuId from HTTP request handlers and write them directly to SSH shell streams to configure the devices.

The problem? The function responsible for making those parameters safe — sanitizeCliInput — only removed newline characters. That's a bit like locking your front door but leaving the window wide open.

Here's the vulnerable code that started it all:

function sanitizeCliInput(val) {
  if (val === undefined || val === null) return '';
  return String(val).replace(/[\r\n]+/g, ' ').trim();
}

At a glance, this looks like it's doing something. It converts the value to a string, strips carriage returns and newlines, and trims whitespace. But it completely ignores the rest of the shell metacharacter alphabet — and that's where the attack lives.


The Vulnerability Explained

What the Code Was Supposed to Do

sanitizeCliInput was clearly written with the intent of preventing command injection by cleaning up user-supplied values before they were sent to SSH shell streams. The sanitizeParams function calls it across all provisioning parameters:

function sanitizeParams(params) {
  const clean = {};
  if (!params || typeof params !== 'object') return clean;
  for (const k of Object.keys(params)) {
    clean[k] = typeof params[k] === 'string'
      ? params[k].replace(/[\r\n]+/g, ' ').trim()
      : params[k];
  }
  return clean;
}

Every string parameter gets the same treatment: strip \r\n, trim. Done. Except it's not done.

Why Newline Stripping Alone Is Not Enough

In a Unix shell, there are many ways to chain or inject commands without ever using a newline character. Here are the metacharacters that the original regex completely ignored:

Character Shell Meaning
; Command separator — run next command regardless of exit code
\| Pipe — send output of one command to another
& Background execution / AND operator
` Backtick command substitution
$() Modern command substitution
<, > Input/output redirection
! History expansion / negation
{, } Brace expansion
\\ Escape character

An attacker doesn't need a newline to inject a command. They just need a semicolon.

A Concrete Attack Scenario

Imagine an HTTP request to the ZTE ONU provisioning endpoint with this payload in the name parameter:

FIBER_USER_001; reboot

After passing through the original sanitizeCliInput:

String("FIBER_USER_001; reboot").replace(/[\r\n]+/g, ' ').trim()
// Result: "FIBER_USER_001; reboot"  ← semicolon survives untouched

The semicolon passes through completely intact. When this string is written to the SSH shell stream connected to a ZTE OLT (Optical Line Terminal), the device's CLI interprets it as two separate commands:
1. Whatever the provisioning command was supposed to do with FIBER_USER_001
2. reboot — which reboots the device, causing a service outage

More sophisticated payloads could:
- Execute delete or no commands to remove existing configurations
- Exfiltrate device configuration via display current-configuration | ...
- Create backdoor accounts on the network device
- Disable entire PON ports, taking down hundreds of subscribers

Because this is a web service, these endpoints are directly reachable by remote attackers — no local access required.

The pon and onuId Parameters Are Also Affected

The vulnerability isn't limited to the name parameter. The sanitizeParams function applies the same incomplete sanitization to all string parameters, meaning pon (e.g., 0/1/1; enable) and onuId (e.g., 5; show running-config) are equally exploitable attack surfaces.


The Fix

What Changed

The fix is a targeted regex expansion in two places — the sanitizeCliInput function and the inline sanitization inside sanitizeParams:

Before:

function sanitizeCliInput(val) {
  if (val === undefined || val === null) return '';
  return String(val).replace(/[\r\n]+/g, ' ').trim();
}

After:

function sanitizeCliInput(val) {
  if (val === undefined || val === null) return '';
  return String(val).replace(/[\r\n;|&`$()<>!{}\\]+/g, '').trim();
}

And in sanitizeParams:

Before:

clean[k] = typeof params[k] === 'string'
  ? params[k].replace(/[\r\n]+/g, ' ').trim()
  : params[k];

After:

clean[k] = typeof params[k] === 'string'
  ? params[k].replace(/[\r\n;|&`$()<>!{}\\]+/g, '').trim()
  : params[k];

Why This Fix Works

The new regex /[\r\n;|&\$()<>!{}\]+/g` covers the full set of shell metacharacters that could be used to inject commands:

  • ; — command chaining
  • | — piping
  • & — backgrounding/AND
  • ` — backtick substitution
  • $ — variable expansion and $() substitution
  • ( and ) — subshell execution
  • < and > — I/O redirection
  • ! — history expansion
  • { and } — brace expansion
  • \\ — escape sequences

Critically, the replacement value also changed: the old code replaced matched characters with a space (' '), which could still cause issues in some edge cases. The new code replaces with an empty string (''), completely removing the dangerous characters.

Two Locations, One Consistent Fix

It's worth noting that the fix was applied in both sanitizeCliInput and the inline logic in sanitizeParams. This is important — having inconsistent sanitization between the two functions would have left a gap where parameters sanitized via sanitizeParams directly (rather than through sanitizeCliInput) could still be exploited. The consistent application of the same regex in both locations closes that gap.


Prevention & Best Practices

1. Prefer Structured APIs Over Shell Strings

The most robust defense against command injection is to avoid constructing shell commands from strings entirely. Where possible, use structured APIs (like parameterized SSH command execution libraries) that separate commands from data at the protocol level. Libraries like ssh2 in Node.js support executing specific commands with argument arrays rather than raw shell strings.

2. Use an Allowlist, Not Just a Blocklist

The current fix uses a blocklist approach (strip known bad characters). While this is a significant improvement, the gold standard is an allowlist: only permit characters that are explicitly known to be safe for your use case.

For ONU provisioning parameters like name, pon, and onuId, the valid character set is likely very narrow — alphanumeric characters, hyphens, forward slashes, and dots. Consider adding an allowlist validation layer:

function sanitizeCliInput(val) {
  if (val === undefined || val === null) return '';
  // Allowlist: only permit alphanumeric, hyphen, slash, dot, underscore, space
  return String(val).replace(/[^a-zA-Z0-9\-\/\._\s]/g, '').trim();
}

This approach is more restrictive but also more future-proof — it won't miss newly discovered metacharacters.

3. Apply Defense in Depth

Sanitization at the application layer is one layer of defense. Consider also:
- Least-privilege SSH accounts: The SSH user used for provisioning should only have permission to run provisioning commands, not arbitrary shell commands.
- Command allowlisting on the device: Many enterprise OLTs support restricting which CLI commands can be executed by specific users.
- Input validation at the API layer: Validate parameter formats (e.g., onuId should be a small integer) before they ever reach sanitizeCliInput.

4. Test Your Sanitization Functions

Sanitization functions like sanitizeCliInput are security-critical code and deserve dedicated unit tests with adversarial inputs:

describe('sanitizeCliInput', () => {
  it('strips semicolons', () => {
    expect(sanitizeCliInput('foo; rm -rf /')).toBe('foo rm -rf /');
  });
  it('strips backticks', () => {
    expect(sanitizeCliInput('foo`whoami`')).toBe('foowhoami');
  });
  it('strips pipe characters', () => {
    expect(sanitizeCliInput('foo | cat /etc/passwd')).toBe('foo  cat /etcpasswd');
  });
  it('strips dollar signs', () => {
    expect(sanitizeCliInput('foo$(id)')).toBe('fooid');
  });
});

5. Relevant Security Standards


Key Takeaways

  • Stripping only \r\n from SSH input is not sufficient sanitization — the original sanitizeCliInput regex gave a false sense of security while leaving the most dangerous metacharacters (;, |, `, &, $()) completely intact.
  • Both sanitizeCliInput and sanitizeParams needed the same fix — inconsistent sanitization between two functions that serve the same purpose creates gaps that attackers can exploit.
  • Network device provisioning endpoints are high-value targets — a successful injection into zteProvisionONU or huaweiProvisionONU doesn't just compromise a server; it can take down physical network infrastructure serving many subscribers.
  • The name, pon, and onuId parameters were all affected — any string parameter flowing through sanitizeParams was an injection vector, not just the most obvious ones.
  • Replacing dangerous characters with empty string is safer than replacing with space — the original code substituted a space, which could still create unexpected command structures in edge cases.

How Orbis AppSec Detected This

  • Source: User-controlled HTTP request parameters (name, pon, onuId) passed to ONU provisioning endpoints (zteProvisionONU, huaweiProvisionONU)
  • Sink: SSH shell stream write operations in services/onuProvisionService.js, where sanitized parameter values are written directly to network device CLI sessions
  • Missing control: The sanitizeCliInput function at line 5 of services/onuProvisionService.js only filtered \r\n characters via /[\r\n]+/g, leaving all shell metacharacters (;, |, &, `, $, (), <>, !, {}, \) intact and injectable
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: The sanitization regex in both sanitizeCliInput and sanitizeParams was expanded to /[\r\n;|&\$()<>!{}\]+/g`, removing all shell metacharacters before values reach the SSH stream

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 vulnerability is a textbook example of how incomplete sanitization can be more dangerous than no sanitization at all — it creates a false sense of security while leaving the door open for attackers. The sanitizeCliInput function in services/onuProvisionService.js was clearly written with security intent, but by only filtering newlines, it missed the entire class of shell metacharacters that make command injection possible.

The fix is elegant in its simplicity: a single regex change in two locations closes the injection path for semicolons, pipes, backticks, subshells, redirections, and escape sequences. But the broader lesson is worth internalizing — whenever you're writing sanitization code for inputs that will touch a shell, CLI, or command interpreter, test it against the full set of shell metacharacters, not just the obvious ones.

For teams building network automation tools, provisioning services, or any system that bridges HTTP APIs to SSH/CLI sessions, this class of vulnerability deserves special attention. The blast radius of a successful injection isn't just a compromised server — it's potentially hundreds of network devices and the subscribers depending on them.


References

Frequently Asked Questions

What is shell command injection?

Shell command injection (CWE-78) occurs when user-controlled input containing shell metacharacters (like `;`, `|`, `&`, `` ` ``) is passed to a shell interpreter without proper sanitization, allowing attackers to execute arbitrary commands.

How do you prevent shell command injection in Node.js?

Avoid constructing shell commands from user input entirely when possible. If you must, use a strict allowlist or strip all shell metacharacters using a comprehensive regex like `/[\r\n;|&`$()<>!{}\\]+/g` before passing input to shell streams.

What CWE is shell command injection?

Shell command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command.

Is removing newlines enough to prevent command injection?

No. Stripping only `\r\n` characters is insufficient. Attackers can use semicolons (`;`), pipes (`|`), ampersands (`&`), backticks (`` ` ``), `$()` subshells, and other metacharacters to inject commands even without newlines.

Can static analysis detect shell command injection?

Yes. Tools like Semgrep, CodeQL, and multi-agent AI scanners can trace tainted user input from HTTP request parameters through sanitization functions to dangerous sink calls like SSH stream writes, flagging incomplete sanitization patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

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

critical

How command injection via execSync() happens in Node.js CLI tools and how to fix it

A critical command injection vulnerability was discovered in `packages/core/bin/cli.js` where the `copyToClipboard` function used `execSync()` with shell command strings. Combined with insufficient filename sanitization in the cache functions, an attacker could inject arbitrary shell commands through malicious repository data containing shell metacharacters. The fix replaces `execSync()` with `execFileSync()` and tightens input sanitization on cache file paths.

high

How shell injection via `${{` variable interpolation happens in GitHub Actions and how to fix it

A high-severity shell injection vulnerability was discovered in `.github/commaSplitter/action.yaml` where unsanitized user input was directly interpolated into a bash `run:` step using `${{ inputs.input }}`. An attacker could craft a malicious input string to escape the shell command and execute arbitrary code on the GitHub Actions runner, potentially stealing secrets and source code. The fix introduces an intermediate environment variable to safely pass the input without shell interpretation.

high

How command injection happens in Ruby backticks and how to fix it

A Jekyll plugin used unsafe Ruby backticks to execute a `git log` command with an unescaped file path, creating a command injection vulnerability. By switching to `Open3.capture2()` with argument array syntax, the fix prevents shell interpretation and eliminates the attack surface entirely.

critical

How credential header disclosure happens in electron-updater and how to fix it

A critical vulnerability in electron-updater (CVE-2026-54673) allowed OAuth tokens and API credentials to leak when HTTP redirects occurred during application updates. The fix upgrades electron-updater from version 6.3.0 to 6.8.9, which properly strips sensitive authorization headers before following redirects to external domains.