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.


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

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

How Command Injection happens in Node.js and how to fix it

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.

high

How command injection happens in JavaScript/Node.js and how to fix it

A build script in a Node.js library used `child_process.exec()` with template-literal-interpolated commit hashes to generate SVG diffs, creating a command injection primitive. The fix replaces `exec()` with `execFile()` and adds strict regex validation of commit hashes before they're used in any shell command.