đŸ›Ąïž Application Security CheatSheet

Command Injection Deep Dive

Command Injection happens when untrusted input is treated as part of an operating system command. Instead of being just “data”, user input changes what the server asks the OS to do.

Production reality: command injection isn’t always ‘user input to shell’ — it’s often a helper function wrapping a CLI where escaping was assumed, not verified.

Key idea: this is not “a shell problem” — it’s a trust boundary problem: user input reaches an OS execution sink without strong separation.

Why it exists (deep reason)

Many apps still call OS tools for real work: image/video processing, PDF generation, Git operations, backups, diagnostics, DNS/network checks, and log rotation. The danger appears when code builds a command as a single string and passes it to a shell.

Interview line: “The root cause is using an interpreter (shell) with untrusted input. The safe pattern is avoiding the shell and passing arguments as an array plus strict allow-lists.”

First-principles mental model

Safe vs unsafe: “array args + shell disabled” keeps structure fixed; “string + shell” lets input influence parsing/execution.

Vulnerable vs secure code patterns (Node.js)

Vulnerable pattern (minimal)

// ❌ Vulnerable: shell interprets a concatenated string
import express from "express";
import { exec } from "child_process";

const app = express();

app.get("/diagnostics/ping", (req, res) => {
  const host = String(req.query.host || "");
  exec("ping -c 1 " + host, (err, stdout) => {
    if (err) return res.status(500).send("Command failed");
    res.type("text/plain").send(stdout);
  });
});

Secure pattern (avoid shell, pass args)

// ✅ Secure: no shell, args passed as an array
import { spawn } from "child_process";

function isAllowedHostnameOrIp(v) {
  // Defensive validation: keep it intentionally strict.
  // Allow only hostnames / IPv4 literals; reject spaces/control chars.
  return /^[a-zA-Z0-9.-]{1,253}$/.test(v);
}

app.get("/diagnostics/ping", (req, res) => {
  const host = String(req.query.host || "");
  if (!isAllowedHostnameOrIp(host)) return res.status(400).send("Invalid host");

  const child = spawn("ping", ["-c", "1", host], {
    shell: false,
    timeout: 3000,
    stdio: ["ignore", "pipe", "pipe"]
  });

  let out = "";
  child.stdout.on("data", (d) => (out += d.toString("utf8")));
  child.on("close", (code) => {
    if (code !== 0) return res.status(500).send("Command failed");
    res.type("text/plain").send(out);
  });
});
experienced note: “Validation is helpful, but the primary control is not invoking a shell and using args arrays. Validation then reduces misuse and edge cases.”

Secure pattern #2 (allow-list commands, never accept raw command)

// ✅ Safe command selection: map user choices to fixed executables/args
const allowedActions = {
  disk: { cmd: "df", args: ["-h"] },
  uptime: { cmd: "uptime", args: [] },
  whoami: { cmd: "whoami", args: [] }
};

app.get("/admin/tools", (req, res) => {
  const action = String(req.query.action || "");
  const tool = allowedActions[action];
  if (!tool) return res.status(400).json({ error: "Unknown action" });

  const child = spawn(tool.cmd, tool.args, { shell: false, timeout: 2000 });
  let out = "";
  child.stdout.on("data", (d) => (out += d.toString("utf8")));
  child.on("close", () => res.type("text/plain").send(out));
});

Where it still happens in modern stacks

Variants (why they differ)

Interview line: “Even without a shell, argument injection can still be severe — passing untrusted flags to safe tools can expose files, change outputs, or trigger network access.”

Detection workflow (experienced-style, systematic)

  1. Find execution sinks: look for child_process.exec, execSync, spawn with shell:true, and any wrapper utilities.
  2. Map inputs to sinks: which request fields/env vars/config values flow into command building.
  3. Classify boundary: string-to-shell vs args-array-to-binary, and whether any arguments are attacker-controlled.
  4. Check constraints: allow-lists, strict validation, fixed command selection, timeouts, and permissions of the executing user.
  5. Assess blast radius: filesystem access, secrets exposure, network egress, container privileges, and available tooling.
Goal: confirm “input can influence command behavior” with minimal, safe evidence — not destructive actions.

Safe validation (defensive verification)

Your proof should focus on control and impact boundaries, not on running arbitrary commands. Prefer demonstrating that input changes command behavior in a harmless way, and capture evidence cleanly.

Evidence checklist (safe)

Professional rule: do not attempt arbitrary command execution in unknown environments. The objective is to show the unsafe boundary and the likely impact with minimal exposure.

Exploitation progression (attacker mindset)

This is a conceptual explanation of how attackers think. It intentionally avoids copy/paste exploit steps.

Phase 1: Discover an execution surface

Phase 2: Determine the execution boundary

Phase 3: Prove controllable behavior safely

Phase 4: Expand impact by chaining

Interview takeaway: attackers escalate from “execution surface” → “boundary type” → “safe control proof” → “impact chain”. Your defense is to remove the shell boundary and lock down arguments and privileges.

Tricky edge cases & bypass logic (conceptual)

Fixes that hold in production

1) Avoid the shell

2) Allow-list actions and arguments

3) Minimize privileges

4) Add guardrails

Regression prevention

Confidence levels (low / medium / high)

Interview-ready summaries (60-second + 2-minute)

60-second answer

Command Injection is when user input reaches OS execution in a way that changes what the server runs. The root cause is usually building a command string and invoking a shell. My first step is to identify sinks like exec or shell:true, then trace which inputs feed them. The production fix is to avoid the shell, pass args as an array, allow-list actions, and run under least privilege with timeouts.

2-minute answer

I treat Command Injection as “untrusted input crossing into an OS interpreter boundary”. In Node, the high-risk pattern is exec or string-based command construction, because the shell parses text and can reinterpret input as control flow. Even without a shell, argument injection can still cause harmful behavior if attacker controls flags or file paths. I validate findings by proving input influences execution behavior safely (repeatable differences) and documenting the execution context and privileges. Remediation is: remove shell usage, use spawn with args arrays, strict allow-lists for actions/args, least privilege execution, timeouts/output limits, and regression prevention via CI checks and tests.

Checklist (quick review)

Remediation playbook

  1. Contain: disable the feature or gate it (admin-only) while fixing.
  2. Identify sinks: inventory all command execution points and wrappers.
  3. Fix boundary: replace string-to-shell with args-array execution; remove shell:true.
  4. Allow-list: map user choices to fixed command templates; validate residual inputs strictly.
  5. Constrain: least privilege, timeouts, output limits, concurrency caps, egress restrictions.
  6. Test: add unit/integration tests that assert rejection of invalid inputs and only allowed actions.
  7. Prevent regressions: CI rules to forbid risky APIs and require reviewed execution helpers.

Interview Questions & Answers (Easy → Hard)

Easy

  1. What is Command Injection?
    A: Plain-English: it’s when user input makes the server run unintended OS commands. Deeply, it’s untrusted input crossing into an OS execution sink (often via a shell).
  2. What is the most common root cause in Node.js?
    A: Plainly: building a command as a string. Deeply: passing attacker-influenced strings to exec or enabling shell:true so a shell parses text.
  3. Is validation alone enough?
    A: Plainly: no. Deeply: validation helps, but the primary control is avoiding the shell and using args arrays with allow-lists.
  4. What’s the best primary fix?
    A: Plainly: don’t use a shell. Deeply: use spawn with shell:false, fixed commands, and strict allow-lists for arguments.
  5. What’s the difference between Command Injection and SQL Injection?
    A: Plainly: different interpreters (OS vs DB). Deeply: same pattern—untrusted input reaches an interpreter and changes structure; defenses are boundary controls and parameterization/allow-lists.
  6. What’s a safe way to prove it?
    A: Plainly: show repeatable behavior change without damage. Deeply: capture baseline vs modified behavior, document sink/boundary, and avoid arbitrary execution.

Medium

  1. Scenario: A “ping” endpoint takes host. What do you worry about?
    A: Plainly: it might run an OS command. Deeply: confirm whether it uses shell execution; if so, user input may change parsing. Fix by using args-array execution plus strict hostname validation and timeouts.
  2. Scenario: App uses spawn but sets shell:true. Why is that risky?
    A: Plainly: it turns safe execution into shell execution. Deeply: the shell reintroduces parsing of special characters, collapsing the “data vs instructions” boundary.
  3. Scenario: No shell is used, but user controls a flag/filename argument. What’s the risk?
    A: Plainly: the tool may behave dangerously. Deeply: argument injection can trigger file reads/writes, network access, or unsafe modes—so you still need allow-lists and strict validation.
  4. Follow-up: How do you reduce blast radius?
    A: Plainly: least privilege. Deeply: run as low-priv OS user, restrict secrets, apply container hardening, limit egress, and add timeouts/output caps.
  5. Follow-up: What code review smells do you search for?
    A: Plainly: command strings. Deeply: exec/execSync, template strings used in commands, shell:true, and wrappers that accept raw user strings.
  6. Scenario: A worker executes jobs from DB config.
    A: Plainly: second-order risk. Deeply: stored input can become execution later; fix via allow-listed job types, strict schemas, and forbidding raw command fields.
  7. Scenario: A feature calls a shell script with interpolated env vars.
    A: Plainly: interpolation can be unsafe. Deeply: the script is an interpreter too; fix by passing arguments safely, quoting properly in scripts, and validating upstream values.

Hard

  1. Scenario: “Safe tool” reads files based on an argument. How can this become high impact?
    A: Plainly: attacker may reach sensitive files. Deeply: even without shell injection, controlling paths/flags can expose secrets. Defend with strict allow-lists, path normalization, and privilege reduction.
  2. Scenario: You can’t see command output. How do you still reason about risk?
    A: Plainly: look for consistent behavior differences and logs. Deeply: prove unsafe boundary (string-to-shell) via code paths, errors, timing, and controlled side effects that are safe to observe.
  3. Scenario: Containerized app claims “it’s fine, it’s in Docker.” Do you agree?
    A: Plainly: not automatically. Deeply: containers can still access secrets, mounts, metadata, and internal networks; impact depends on privileges, mounts, and egress controls.
  4. Follow-up: What is your long-term prevention strategy?
    A: Plainly: standardize safe execution. Deeply: a single audited helper module, CI rules banning risky APIs, templates for allow-lists, and tests/telemetry for all tool executions.
  5. Follow-up: How do you handle “we must run OS tools” requirements?
    A: Plainly: run them safely. Deeply: fixed executables, args arrays, strict allow-lists, sandboxing, least privilege, and timeouts/output quotas.
  6. Scenario: A “convert” feature uses a third-party library that calls OS tools internally.
    A: Plainly: hidden sinks exist. Deeply: review library behavior, disable shell use if possible, constrain inputs, sandbox the worker, and monitor executions for anomalies.
  7. Scenario: You fixed code to use spawn but kept user-controlled executable name. Still safe?
    A: Plainly: no. Deeply: controlling executable selection is dangerous; commands must be fixed/allow-listed, and PATH should not be attacker-influenced.
  8. Follow-up: What metrics/logs help detect abuse?
    A: Plainly: tool usage anomalies. Deeply: log action name, exit codes, timeouts, frequency per user/IP, and alert on spikes or repeated failures without logging sensitive arguments.
Safety note: for learning and This guide focuses on understanding, defensive validation, and production-grade remediation without providing exploit recipes.