hunting-command-argument-and-flag-injection · git:20260903.6dac719 · 2026-09-03 · sha256 e530cdd21b7df083

hunting-command-argument-and-flag-injection git:20260903.6dac719A

Immutable. This exact content is served forever at /api/v1/blob/e530cdd21b7df083.

---
name: hunting-command-argument-and-flag-injection
description: >-
  Hunt argument and flag injection where untrusted input occupies a slot in a subprocess argument vector,
  with no shell involved, and the target program parses it as an option rather than data. Covers a value
  that starts with a dash and becomes a flag, and a value inserted before the program separates options
  from operands, turning a data argument into one that changes behavior: writing or reading a file, running
  an embedded command, changing a config or protocol, or reaching a network target. This is not command
  injection, there is no shell, so metacharacter defenses miss it; the fix is an argument terminator and a
  fixed positional layout. Use when input is placed into an argv slot of a spawned tool. The untrusted argv
  value is the source, the target program option parser is the sink, and an argument reinterpreted as a
  dangerous option is the bug.
license: MIT
---

# Hunting command argument and flag injection: when a value becomes an option

Argument injection lives in the safe-looking case: a program is spawned without a shell, as a fixed binary
with a separate argument vector, so there is no metacharacter interpretation and command injection does not
apply. The bug is that the target program parses its own arguments, and an attacker-controlled value placed
in the vector can be read as an option rather than as data. A value that begins with a dash becomes a flag; a
value inserted where the program has not yet seen the options-terminator can turn on a behavior the caller
never intended, writing to a file, reading one, executing an embedded command, changing an output or a
protocol, or contacting a network host. Because there is no shell, defenses aimed at metacharacters do
nothing. You find it by locating shell-less spawns whose argument vector includes untrusted values and asking
whether the target can parse those values as options.

## When to use

- Code spawns a fixed program with an argument vector (no shell) that includes untrusted input.
- The target program accepts options that read or write files, run commands, or change its destination.
- Input can start with a dash, or is inserted before a fixed set of trailing operands.

## Scope check

Test argument injection only against systems you own or are authorized to assess, on non-production hosts,
because a confirmed case makes a real tool take an unintended action. Prove the class with an inert option
whose effect is observable but harmless, and stay within the authorized host. If you can't name the
authorization, stop.

## The loop

1. **Establish that input can occupy an option slot first.** For each shell-less spawn, determine whether an
   untrusted value can start with a dash, or be positioned in the vector before the program has seen an
   options terminator, so the target parses it as an option rather than an operand. This is the
   false-positive killer: a value pinned after a `--` terminator, or in a fixed operand position the program
   treats as data, cannot become a flag. Confirm the value can reach an option-parsed slot before going on.

2. **Identify the target program and its dangerous options.** Name the spawned tool and read, generically,
   which of its options change behavior in a security-relevant way: writing output to an arbitrary path,
   reading or including a file, executing a subcommand or hook, changing the destination or protocol, or
   loading a config. The impact is defined by what options the specific tool exposes.

3. **Trace the untrusted value into the vector.** Follow the request or stored input into the exact argv
   element it becomes, and confirm the code does not sanitize a leading dash, reorder operands, or insert a
   terminator. A value that the caller believes is a filename or a search term, placed directly into the
   vector, is the vector.

4. **Check the options-terminator and positional layout.** Determine whether the spawn inserts a `--`
   terminator before untrusted operands and whether the tool honors it, and whether the layout fixes
   untrusted values in positions the tool treats as data. A missing terminator, or a tool that keeps parsing
   options after operands, leaves the slot exploitable.

5. **Assess reachable impact for this tool.** From a value that can be an option, determine what it actually
   enables for the target: an arbitrary-write flag, a file-read or include flag, a command-execution or hook
   option, or a network or protocol switch. This is where severity is decided; assess it from the tool's
   option surface, not by launching destructive flags.

6. **Confirm and record.** Confirm by supplying a value that a benign, observable option would trigger (an
   output written to an allowed location, a version banner, a bounded delay) through the input on an
   authorized host, showing the tool parsed it as an option. Kill the lead if the spawn inserts a `--`
   terminator the tool honors before untrusted operands, if the value is pinned to a data-only position, if
   a leading dash is rejected or neutralized, or if the tool exposes no behavior-changing option. Record the
   input, the spawn and target tool, the option the value became, and the reachable effect.

## Where argument injection leaks

- **There is no shell, so shell defenses miss it.** Quoting and metacharacter filtering aim at a shell that
  is not present; the bug is in the target program's own option parser.
- **A leading dash is the trigger.** A value that starts with a dash is read as a flag by most tools unless a
  terminator or a fixed operand position prevents it.
- **The missing terminator is the root cause.** Without a `--` before untrusted operands, the tool cannot
  tell the caller's data from options.
- **Impact is the tool's option surface.** A tool with a write-to-file, read-file, exec-hook, or
  change-destination option turns a data argument into that action.
- **Callers assume the value is data.** The code treats the value as a filename or a query, but the target
  treats a dash-prefixed value as configuration.

## Worked example (a confirm and a kill)

> **Confirm.** A repository feature spawns a fixed version-control binary with a user-supplied ref placed
> directly in the argument vector and no options terminator. A ref that begins with a dash is parsed by the
> tool as an option that runs an external helper, and an inert helper that writes an observable marker to an
> allowed path executes. **Confirmed** argument injection reaching command execution through a tool option,
> `high`, remediation = insert a `--` terminator before untrusted operands, reject values beginning with a
> dash, and fix untrusted values to positions the tool treats as data.
>
> **Kill.** The same spawn places the ref after a `--` terminator that the tool honors, validates the ref
> against an allowlist pattern, and rejects a leading dash. A dash-prefixed ref is delivered as a literal
> operand and the tool reads it as data. **Killed**, `kill_reason` = "spawn inserts an honored options
> terminator before the untrusted operand and rejects leading dashes; the value cannot be parsed as an
> option."

## Rationalizations to reject

- *"There is no shell, so it is safe."* -> The absence of a shell rules out command injection, not argument
  injection; the target program parses its own options from the vector.
- *"We escape shell metacharacters."* -> Metacharacter escaping is irrelevant with no shell; a plain
  dash-prefixed value is the whole problem.
- *"It is only a filename."* -> A filename that starts with a dash is an option to most tools; the caller's
  intent does not bind the target's parser.
- *"The tool is harmless."* -> Assess the tool's actual option surface; many common tools have a write, read,
  include, hook, or destination option that changes behavior.
- *"We put it at the end of the arguments."* -> Position alone does not help unless a terminator precedes it
  and the tool stops parsing options after operands.

## Executing this in practice

You need every shell-less spawn whose argument vector includes untrusted input, the target program and its
behavior-changing options, whether a `--` terminator is inserted and honored, and whether leading dashes are
rejected. For each, decide whether the value can reach an option-parsed slot and what option it could become.
Reading the spawn shows the vector layout and terminator; supplying a value that a benign observable option
would trigger shows whether the target parsed it as an option.

## Related

- `hunting-os-command-injection` - the sibling for shell-mode spawns, where the bug is metacharacter
  interpretation rather than an option parser; this skill covers the shell-less case.
- `hunting-scheduled-job-and-search-path-hijacks` - a related spawn-behavior class where the environment and
  path, rather than an argument, change what runs.
- `hunting-path-traversal-and-file-access` - a frequent impact when the injected option is a read or write
  flag pointed at an arbitrary path.
- `adjudicating-taint-paths` - use it to prove an untrusted value reaches an option-parsed argv slot.
- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the untrusted argv value, sink = the target
  program option parser, evidence = a benign observable option triggered by the injected value.