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
•Technically reviewed by Anupam Mediratta•Published August 1, 2026•Reviewed 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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.