RedTeam Nexus Labs
Module 12 · Lesson 03 // Command Injection

argument handling

Command Injection · CTF

Freereading · 10 min
On this page
LESSON

Overview

The most reliable defense against command injection is not clever filtering — it is argument handling: passing untrusted values as separate, discrete arguments to a program, rather than pasting them into a command string a shell will parse. This lesson explains how argument arrays and execFile-style APIs work, why they neutralize metacharacters, and how to structure code so input can never become code. As always, examples are illustrative and meant for authorized, isolated practice.

Learning objectives

  • Explain how an argument array (argv) differs from a shell command string.
  • Describe why passing arguments discretely makes metacharacters inert.
  • Use execFile-style / no-shell APIs to run external programs safely.
  • Understand the limits of argument handling and where extra validation is still needed.

Core concept

When an operating system starts a program, the program receives its inputs as an argument vector — commonly called argv — which is simply an ordered list of strings. argv[0] is the program name; argv[1], argv[2], and so on are its arguments. There is no shell in this path. The kernel hands the program exactly the list you provided, byte for byte.

This is the key insight: if you deliver a user value as one element of that list, it arrives at the target program as a single, literal string. Characters like ;, |, $(...), and * have no special meaning because no shell ever parses them — they are just bytes inside one argument. The data/code boundary is preserved automatically, because you never built a string for an interpreter to re-parse.

How it works

Language runtimes expose "run a program directly" APIs — often named things like execFile, execv, spawn (without a shell), or a subprocess call with a list of arguments and no shell=true flag. You call them by naming the executable and providing the arguments as a list/array:

  • The runtime passes the array straight to the OS as argv.
  • No /bin/sh -c "..." wrapper is created.
  • Each array element becomes exactly one argument, regardless of its contents.

Because the shell is never invoked, shell features (pipes, redirects, globbing, chaining, variable/command substitution) are also unavailable — which is exactly what you want when running with untrusted input. If you genuinely need those features, you construct them explicitly and safely in code (e.g., open files yourself for redirection, pipe processes together with the runtime's own APIs) rather than delegating to a shell string.

Practical example

A lab service looks up DNS records for a hostname the user supplies.

# SAFE (illustrative) — program name + argument array, no shell
run_program("nslookup", [user_hostname])
  • Input example.test → nslookup receives one argument, example.test.
  • Input example.test; cat /etc/passwd → nslookup receives one argument whose value literally contains a semicolon and spaces. nslookup will simply fail to resolve that odd "hostname." No second command runs, because there is no shell to see the ;.

Compare to the unsafe version from the previous lesson, where the same input would have chained a second command. The only thing that changed is argument handling: a list instead of a concatenated string, and no shell.

A subtle but important point: argument handling stops shell interpretation, but it does not validate meaning. If the program itself treats a leading - as an option, an argument like --some-dangerous-flag could still change behavior (an "argument injection" concern). So combine argument arrays with input validation and, where a program supports it, the -- end-of-options separator.

# Even safer: validate, and stop option parsing with `--`
if not is_valid_hostname(user_hostname): reject()
run_program("nslookup", ["--", user_hostname])

[!NOTE] Test these behaviors only against tools and data in a sandbox you own. The goal is to internalize why argument arrays are safe, so you write them by default.

Cybersecurity relevance

Argument handling is the defense that scales. Filtering and escaping require you to anticipate every metacharacter, encoding, and shell quirk; passing discrete arguments sidesteps the entire class of shell-parsing bugs by construction. Secure-coding guidance across the industry recommends the same order of preference: avoid invoking a shell; call the program directly with an argument array; validate input; and only if a shell is unavoidable, escape rigorously with a vetted library. For authorized testers, code that consistently uses execFile-style calls with argument arrays is a strong signal that command injection is unlikely on that path.

Common mistakes

  • Building one big string and then splitting it on spaces to make an "array" — this re-introduces parsing bugs; construct the array element by element.
  • Falling back to a shell just to get a pipe or redirect, discarding the safety you gained.
  • Forgetting argument/option injection — a value beginning with - may be read as a flag; use -- and validate.
  • Passing user input as the program name rather than as an argument — the executable should be fixed/allowlisted, never user-chosen.

Key takeaways

  • Programs receive inputs as an argument vector (argv) — an ordered list of literal strings, with no shell involved.
  • Passing untrusted values as discrete array elements (execFile-style / no-shell APIs) makes shell metacharacters inert by construction.
  • This avoids the entire shell-parsing bug class, which is why it is preferred over escaping or filtering.
  • Argument arrays stop shell injection but not argument/option injection — still validate input, fix the executable, and use -- where supported.

Check your understanding

  • Why does placing a value like example.test; id into an argument array (rather than a command string) prevent a second command from running?
  • Argument arrays stop shell interpretation — what additional risk remains, and how do you mitigate it?
FINISH THIS LESSON

Sign in to save your progress.