Web Security (OWASP Top 10:2025) Module 6 – A05:2025 Injection
OS command and code injection
Why a shell string turns input into commands, how eval and compiled templates run input as code, and the argument-list and containment fixes.
What you will learn
- Explain shell injection and argument injection (CWE-78 and CWE-88) and why command strings are dangerous
- Replace shell strings with argument lists, and allowlist the values they take
- Recognise code-injection sinks such as eval, Function, vm, pickle and template compilation, and avoid them
- Use limits on environment, time, output and rights as defence in depth when a program must run
Before you start
What you’ll learn · Before you start
What you will learn
- Explain shell injection and argument injection (CWE-78 and CWE-88) and why command strings are dangerous
- Replace shell strings with argument lists, and allowlist the values they take
- Recognise code-injection sinks such as eval, Function, vm, pickle and template compilation, and avoid them
- Use limits on environment, time, output and rights as defence in depth when a program must run
Before you start
On this page
OS command injection: A weakness in which input becomes part of a command line that a shell or the operating system runs, so one value can start a command the author never wrote. MITRE files it as CWE-78. is ninth and code injection: A weakness in which input becomes part of text that is compiled and run as code, through eval, new Function, a template built from input or a similar call. MITRE files it as CWE-94. tenth in MITRE’s 2025 CWE Top 25,
and the OWASP Top 10:2025 uses a lookup tool as one of the three scenarios of A05 Injection: a domain request
parameter pasted into a command line. When a shell reads that line, one value can start a second command that nobody
wrote. The cure is the one this module keeps returning to, keeping data out of code: give a program its input as an
argument list: The separate arguments handed to a program when it is started, such as echo, checking and a host name. No shell reads them, so a space or a semicolon inside one is only a character in it. that no shell reads.
You will watch a shell split one string into two commands, see why a value that starts with a dash still becomes an
option when no shell is involved, test a launcher against a fake shell, replace eval with a small evaluator, and limit
what a program can do when it has to run.
A shell reads your string as a program, so pass an argument list
A diagnostics page lets staff type a host name and runs a lookup tool for it. The program below stands in for the tool
with echo, so nothing it starts can do any harm. It tries two hosts: an ordinary one, and one followed by a
semicolon and a marker, CANARY-2, which a lookup has no use for.
// The same lookup, written two ways, for a host name that staff type into a diagnostics page. echo stands in for the
// real lookup tool, so nothing started here can do any harm.
import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util';
const viaShell = promisify(exec);
const viaList = promisify(execFile);
const lines = (text) => text.trimEnd().split('\n');
async function lookupWithShellString(host) {
const { stdout } = await viaShell('echo checking ' + host);
return lines(stdout);
}
async function lookupWithArgumentList(host) {
const { stdout } = await viaList('echo', ['checking', host]);
return lines(stdout);
}
for (const host of ['example.org', 'example.org; echo CANARY-2']) {
console.log(`host: ${host}`);
for (const [name, lookup] of [['exec', lookupWithShellString], ['execFile', lookupWithArgumentList]]) {
const out = await lookup(host);
console.log(` ${name.padEnd(8)} printed ${out.length} line${out.length === 1 ? '' : 's'}: ${JSON.stringify(out)}`);
}
} Output
host: example.org exec printed 1 line: ["checking example.org"] execFile printed 1 line: ["checking example.org"] host: example.org; echo CANARY-2 exec printed 2 lines: ["checking example.org","CANARY-2"] execFile printed 1 line: ["checking example.org; echo CANARY-2"]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node exec_vs_execfile.mjs
The ordinary host prints one line both ways. The second host made exec print two lines, checking example.org and
then CANARY-2. That second line is OS command injection, which runs commands the author never wrote. Here exec
gave the whole string to a shell, and a shell reads ; as the end of one command and the start of another.
execFile starts the program itself and passes it two arguments, the second being the host exactly as typed, so the
semicolon is only a character in a name. Node’s documentation warns against passing unsanitised input to exec, and
gives the same warning for execFile and spawn when their shell option is on. Step through the difference:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A host arrives. Staff type example.org; echo CANARY-2 into the form. It is one ordinary string, and nothing about it matters until some code hands it to a program that reads it.
- exec builds a string. The code joins "echo checking " and the host into one command string and passes the whole string to exec.
- The shell reads it. A shell treats a semicolon as the end of one command and the start of the next, so it finds two commands in the text, not one.
- Two programs start. The shell starts echo twice. The first prints checking example.org, and the second prints CANARY-2, a line that no one wrote into the page's code.
- execFile builds a list. With execFile the code names the program and gives its arguments as a list, here checking and the host exactly as typed. There is no command string for anyone to split.
- Nobody reads the text. The operating system starts echo with those two arguments. The semicolon is only a character inside the second one, and the program prints one line.
A shell does much more than split at ;. It expands variables and wildcards, runs $(...) and backticks, redirects
output with > and joins commands with &&, ||, | and line breaks, so no list of forbidden characters is ever
complete. In code injection (CWE-94) the reader is not a shell but something that compiles and runs text, such as
eval, new Function, or a template engine given a template made from input. It is the same mistake the module has
followed since SQL injection, with a different reader
at the end, and the defence is the same: do not build the text at all. Use a library call when one exists, and
otherwise an argument list with no shell. ASVS 5.0.0 requirement 1.2.5 asks the same of operating system calls:
parameterise them, or encode for the command line.
Key idea
Give a program its input as an argument list, and no shell ever reads it.
Checkpoint
Check: A shell reads a string as commands
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is OS command injection, and how do you prevent it?
A value pasted into a command line is read by a shell, which reads ; as the point where one command ends and the next begins, so a host name followed by a semicolon and a marker ran a second command nobody wrote (ninth in MITRE's 2025 CWE Top 25). Give the program its input as an argument list that no shell reads.
-
In Node, what is the difference between exec and execFile for a host name followed by a semicolon?
exec gives the whole string to a shell, which ran the second command and printed the marker on a second line. execFile starts the program itself and passes the host exactly as typed as an argument, so the semicolon is only a character in a name. Node warns against unsanitised input to exec, and to execFile and spawn when their shell option is on.
-
Why can no list of forbidden characters make input safe for a shell?
A shell also expands variables and wildcards, runs $(...) and backticks, redirects output with >, and joins commands with &&, ||, | and line breaks. Use a library call when one exists, and otherwise an argument list with no shell (ASVS 5.0.0 1.2.5 asks to parameterise operating system calls or encode for the command line).
-
What is code injection (CWE-94), and how is the defence the same?
The reader at the end is not a shell but something that compiles and runs text, such as eval, new Function, or a template engine given a template made from input. It is the same mistake with a different reader, so the defence is the same: do not build the text at all.
A value that starts with a dash is an option, even without a shell Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Test a launcher against a fake shell that records what would run
- Text handed to eval runs as code, so read it with a small evaluator
- A program that must run gets limits on its environment, time, output and rights
- Practise: fix two launchers behind a fake shell
- Two launchers that pass arguments, not strings
- Interview questions
- What is OS command injection, and why does passing an argument list prevent it?
- A reporting service builds one string, a converter program with a file name and a size, and passes it to exec(). Both values come from a form, and a code scan flags the call. How do you fix it, and what do you check besides the shell?
- A teammate closes a scan finding with "we use execFile, so command injection is impossible". The call passes a user's file name as the only argument to an archive tool. What can still go wrong, and what do you change?
- A pricing screen evaluates formulas typed by staff with eval(). Product wants to keep the feature and add conditionals and a few functions. Walk through your options and the one you would ship.
- Key takeaways
5 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 38 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- argument injection
- A weakness in which a value that begins with a dash is read by a program as one of its options instead of as the data it was meant to be, even when no shell is involved. MITRE files it as CWE-88.
- argument list
- The separate arguments handed to a program when it is started, such as echo, checking and a host name. No shell reads them, so a space or a semicolon inside one is only a character in it.
- code injection
- A weakness in which input becomes part of text that is compiled and run as code, through eval, new Function, a template built from input or a similar call. MITRE files it as CWE-94.
- fake execution layer
- A test double for the calls that start programs. It records the program and arguments that would run instead of running them, so a test can check that each input stayed one argument of one command.
- OS command injection
- A weakness in which input becomes part of a command line that a shell or the operating system runs, so one value can start a command the author never wrote. MITRE files it as CWE-78.
References
- A05:2025 Injection (OWASP Foundation)
- OS Command Injection Defense Cheat Sheet (OWASP Foundation)
- 2025 CWE Top 25 Most Dangerous Software Weaknesses (MITRE)
- CWE-88: Improper Neutralization of Argument Delimiters in a Command ('Argument Injection') (MITRE)
- CWE-94: Improper Control of Generation of Code ('Code Injection') (MITRE)
- Child process, Node.js v24 documentation (OpenJS Foundation (Node.js))
- Deprecations (DEP0190), Node.js v24 documentation (OpenJS Foundation (Node.js))
- VM (executing JavaScript), Node.js v24 documentation (OpenJS Foundation (Node.js))
- Permissions (the Permission Model), Node.js v24 documentation (OpenJS Foundation (Node.js))
- Command-line API (--disallow-code-generation-from-strings), Node.js v24 documentation (OpenJS Foundation (Node.js))
- subprocess: subprocess management, Python 3.14 documentation (Python Software Foundation)
- ast: abstract syntax trees (literal_eval), Python 3.14 documentation (Python Software Foundation)
- pickle: Python object serialization, Python 3.14 documentation (Python Software Foundation)
- ASVS 5.0.0, V1 Encoding and Sanitization (requirements 1.2.5 and 1.3.2) (OWASP Foundation)
- RFC 1123: Requirements for Internet Hosts, Application and Support (IETF (RFC Editor))
- RFC 1035: Domain Names, Implementation and Specification (IETF (RFC Editor))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress