Course / 09 Web security / Lesson 04 of 28

Command injection

Estimated time: 16 min reading · 12 min activity

An application may ask the operating system to run a program. Command injection occurs when untrusted input can change the command being executed. A shell parses a command line, treating certain punctuation as instructions. An argument is one value passed to a program; an argument list keeps those values separate from shell command text.

For this POSIX example, Python's subprocess.run can start a fixed program directly with shell=False. The brackets make a Python list; each quoted item is one string. The safer design is to use a suitable library operation instead of starting an external program when that is practical. If a process is necessary, fix the program and validate the arguments for that program.

Before you begin

Estimated time
16 min reading, 12 min activity. Setup and optional practice may take longer.
Where you work
Read the original fictional requests, source fragments and state tables in a current desktop or mobile browser. The required activity needs no server, database, account or installed security tool. Do not send the printed requests to any service.
Without a lab
Use the complete printed evidence to trace the data or state on paper. Write the decision, the fact supporting it and one limit of the model, then compare with the activity rubric. This does not establish live service behavior. Reading works without JavaScript. Checking requires JavaScript and a secure browser context; saving also requires usable browser storage.

After this lesson you can:

  • Name the program, the format, and the value in the safe call.
  • Say whether the activity call starts a shell parser.
  • Choose the pattern whose arguments stay separate.

01Example

Read this fresh fictional direct-process call, then use the supplied club-note source as the guided activity. Neither call is run here.

subprocess.run(["printf", "%s", "memo-card"], check=True, shell=False)
Program: printf
Fixed format: %s
Value: memo-card
Predicted output: memo-card

02A list does not invite a parser

In the demonstration, printf is the program and %s is its fixed format: print the next argument as text. The value memo-card is a separate item. check=True asks Python to report a nonzero exit status as an error; shell=False keeps a shell from parsing a command line. The result shown is a prediction, not a fresh execution.

The boundary is caller data entering an instruction parser. Avoiding shell parsing addresses that boundary but does not make every program or argument safe. A program may interpret an argument as an option or perform an unwanted action itself. Windows batch files also have shell-handling differences; this card does not establish their behavior.

Keep a program name and its arguments in a list so a shell never parses the caller's value.Model. A teaching model drawn by hand. It shows the idea, not real tool output.

Read the full explanation below.

No shell parser

subprocess.run(["printf", "%s", "memo-card"], check=True, shell=False)
Program: printf
Fixed format: %s
Value: memo-card
Predicted output: memo-card
Program argumentFixed format argumentValue argumentNo shell parser
What this part means

The argument list separates printf, its fixed %s format and the value memo-card.

The printed output is a prediction, not a subprocess run.

The argument list separates printf, its fixed %s format and the value memo-card. The printed output is a prediction, not a subprocess run.

Follow the explanation

  1. The argument list separates printf, its fixed %s format and the value memo-card. The printed output is a prediction, not a subprocess run.
  2. shell=False keeps this call from asking a shell to parse the value. The card does not establish that every possible program treats every argument safely.
Read every state and value

1. The argument list separates printf, its fixed %s format and the value memo-card. The printed output is a prediction, not a subprocess run.

Selected parts
Program argument · Fixed format argument · Value argument
Explanation
args

2. shell=False keeps this call from asking a shell to parse the value. The card does not establish that every possible program treats every argument safely.

Selected parts
No shell parser · Value argument
Explanation
parser
Avoiding shell parsing protects this boundary; it does not make every program or argument safe.

03Read the safe call

Read the evidence before comparing your reasoning with the activity rubric. Do not run the file.

Original comparison. The shell form is a description, not a program.

Safe pattern, also saved as safe-print.py:
  subprocess.run(["printf", "%s", "club-note"], check=True, shell=False)
  The program name is printf.
  The next item is the format.
  The last item is the value club-note, one argument.
  shell stays false, so this call does not start a shell parser.

Shell form, not saved as a program and not to be typed:
  one string would hold the program and the value together
  a shell would parse that string as a command line
  characters that are syntax to that shell would not stay plain data

Predicted result of the safe call, not observed:
  the process writes club-note and nothing else

Download the boundary card

"""Fixed program and separate arguments. Authoring did not run this file.

A later run, if separately authorized, may use only the value below.
Do not replace the list with a shell string.
"""
import subprocess

value = "club-note"
subprocess.run(["printf", "%s", value], check=True, shell=False)

Download the fixed-argument script

  1. Count the items in the list passed to run. Write the program name and the value.
  2. Read the shell argument on that call.
  3. Check: the value club-note is one list item, and the call does not start a shell parser. The predicted write is club-note and nothing else. That write was not observed.
  4. Stop. Do not replace the list with a shell string.

04Optional challenge

Explain why a friendlier error page leaves the shell boundary where it was. The rubric is: the page is output, and the parser decision is the call shape. About ten minutes.

05Fresh cases for the checks

Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.

Process case A: fresh direct call, not runOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

value = "field-card"
subprocess.run(["printf", "%s", value], check=True, shell=False)
The supplied model treats printf as a fixed native program.

Process case B: independent call descriptionsOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

P: fixed program and separate list arguments, shell=False.
Q: combined command string passed with shell=True.
R: fixed program and separate list arguments, shell=False, caller value begins with a dash.
No call is executed. Avoiding a shell does not decide whether a program accepts an argument safely.

06Explain your evidence

Optional written self-check: explain one decision using the supplied case. No tool run is required.

Check your explanation: name every direct-call component; state whether a shell parser is requested; distinguish shell syntax from program argument meaning; preserve fixed program and separate data arguments.

07Common mistakes

If you read %s as the caller's value, separate the fixed format item from the following value. The list contains the program first, then two arguments.

If you expect a shell to interpret punctuation inside the value, read shell=False. For this POSIX direct-process model the value remains one argument.

A predicted output is not evidence of a run. The source and the card are sufficient for this activity; platform behavior remains a separate practical check.

References

  • Python 3.12: subprocess

    run() defaults shell to False. The arguments and Security Considerations sections explain direct argument sequences, shell parsing and platform differences, including Windows batch handling. The displayed output is a prediction rather than an observed process result.

  • PortSwigger: OS command injection

    Explains the failure mechanism and prevention. Use it for further reading after reasoning from this page's original model; a topic page is not evidence that a lab was launched.