Injection turns untrusted data into instructions. Unsafe name resolution directs a program to a file, executable, or library the attacker chose. Containment assumes that some program will be compromised anyway and limits what it can see, do, and consume.
Command Injection
An interpreter is any component that parses text into actions: a database engine, a shell, an expression evaluator, a template engine, or a logging library. Injection occurs when input from outside the program reaches an interpreter as part of a command and the interpreter reads some of it as instructions.
Three conditions produce injection:
-
An interpreter.
-
Input the program does not control.
-
A command built by pasting that input into a string.
Removing any one removes the attack, and most defenses remove the third.
The root cause is that the program does not separate data from instructions. Only the interpreter knows which characters are syntax, and it cannot tell which characters the programmer wrote.
Command injection does not need memory bugs. The program’s own interpreter runs the attacker’s command with the program’s privileges, so the defenses against memory corruption do not apply.
SQL Injection
A query built by string concatenation lets a quote in the input close the programmer’s string. The rest of the input is then treated as SQL. A comment marker will discard whatever the programmer wrote after it. This can allow an attacker to bypass a login, read or modify other tables, and, in some databases, reach files or programs on the server.
Two variations extend the basic attack:
-
Blind SQL injection extracts data without displayed results by observing whether a page changes or how long a response takes.
-
Second-order injection stores data safely and uses it unsafely later. Safety is a property of each use of the data, not of the moment it arrived.
The defenses fall into an order of preference:
-
Parameterized queries (prepared statements) separate commands from parameters (which may be user-supplied). They send the query structure to the database before the data, so the data is bound to placeholders after parsing and can never be read as syntax. This is the defense to use whenever input appears in a query.
-
Stored procedures give the same protection unless the procedure builds a query string from its arguments. The procedure is in the database engine and the program calls a function instead of building a query.
-
Allowlists handle the parts of a query that cannot be parameters, such as table and column names: the application checks the user’s choice against a fixed set and uses its own copy.
-
Escaping is a last resort, because the rules differ between databases and character encodings can hide a quote from the escaping function.
-
The Principle of Least Privilege, applied to the database account, limits what an injected query can do.
Syntax, special characters, and available operations differ among databases, so a filter or escaping function written for one might be wrong for another.
Document databases that take queries as structured objects are subject to operator injection: an object containing a query operator is supplied where a string was expected. The defense is to check or convert types before the value reaches the query.
Shell Command Injection
The standard C library functions system(), popen(), and their equivalents in other languages pass a string to sh -c, which gives the shell’s whole language to the attacker. Shell metacharacters separate commands, pipe output, run commands conditionally, substitute a command’s output into the line, redirect input and output, and expand wildcards. Filtering can be error-prone and double quotes still allow command substitution inside the string.
The defenses, in order:
-
Do not invoke a shell. Use a library call, or start the program directly with an argument vector (
execve(), orsubprocess.run()with a list), so that each argument arrives as one string that no shell parses. -
Guard against argument injection, since the program being run still parses its own command line and a value beginning with
-can be read as an option. For some programs. the fixes are--to end options, rejecting option-like values, or prefixing a file name with./. -
If a shell is unavoidable, fix the command structure, validate each value, and quote with a function written for that shell. Quoting is secondary because the rules differ between shells and the shell is not always the only program that parses the arguments.
-
Apply the Principle of Least Privilege, so that an injected command inherits as little authority as possible. This is a design principle rather than a fix for one bug, and it limits the damage from every bug that the other defenses miss.
Interpreters Hidden in Libraries
eval() takes a string and runs it as code in the program’s own language, so input meant to be a number or an expression can contain function calls, imports, or a command that starts a shell.
The difference from shell command injection is the interpreter. The shell parses a command line, while eval() sends the input to the language interpreter itself, with every library the program has loaded. The fix is the same: parse the input as a number or with a data parser, which cannot run anything, rather than evaluating it.
Libraries also contain interpreters that programmers do not know about:
-
A logging library that expands lookup expressions inside logged strings turns every logged field into an attack vector.
-
Data formats that can describe objects to construct run code when loaded.
Validating and Sanitizing Input
Three terms describe how input is handled before it reaches an interpreter:
-
Validation accepts or rejects a value according to a rule.
-
Sanitization transforms a value, by escaping or removing characters, so an interpreter reads it as data.
-
Structural separation supplies data through a channel the interpreter does not parse, such as a query parameter or an argument vector (see parameterized queries, above).
Separation is preferred because it does not depend on predicting the interpreter. Validation is still needed after it, since a bound value can be the wrong value.
An allowlist defines what is permitted and rejects everything else. A denylist names what is forbidden and permits everything else, which makes it a prediction about what the attacker will write, and an interpreter can construct the forbidden string after the filter has run.
Canonicalization converts every equivalent representation of an input into one chosen form. The rule is to decode and canonicalize first, then validate, and never decode again, since a check on one representation does not cover the one the interpreter will see.
Four more rules follow from the same idea:
-
Sanitization has to match the context of use, because SQL, a shell, HTML, and a file path have different special characters.
-
Validating at a program’s entry points and trusting everything inside fails for the same reason second-order injection does.
-
Checks in the browser are bypassed by sending the request directly.
-
Length limits do not stop injection.
Path Traversal and Path Equivalence
Path traversal uses .. in a supplied path to reach files outside the intended directory. The component can appear anywhere in the path and can be encoded so a check made before decoding does not see it. Searching for the characters is not foolproof, since legitimate names contain dots and a .. that stays inside the tree is harmless. The property to enforce is where resolution ends.
Path equivalence is the fact that many strings may refer to the same file: through . and redundant separators, case-insensitive filesystems, Unicode normalization, symbolic links, and hard links. A symbolic link makes a path that passes every textual check resolve somewhere else.
The defenses, from weakest to strongest:
-
Canonicalize with
realpath()and check the prefix, including its trailing separator. -
Have the kernel enforce the restriction during resolution, so
..and links cannot leave a directory. -
Map an identifier to a file instead of accepting a path.
-
Run with permissions that limit what a traversal reaches.
Time-of-Check to Time-of-Use
A TOCTOU race condition occurs when a program checks a name and then uses it, and the binding between the name and the object changes in between, usually through a symbolic link or directory entry the attacker controls.
Two cases are the classic ones:
-
The
access()thenopen()sequence in a setuid program. -
Temporary files: generating a name and then creating the file lets an attacker create the file, or a link under that name, first.
The defenses share one rule, which is to operate on the object rather than on the name:
-
Check and create in one step, with
mkstemp()oropen()withO_CREAT | O_EXCL. -
Open once and perform every later operation through the descriptor, which refers to the file regardless of its name.
-
Attempt the operation and handle failure rather than checking first. A setuid program switches its effective user ID to the caller’s so the kernel makes the check during the open.
-
Refuse to follow symbolic links where none is expected.
Privilege separation removes the race condition by having a process with the user’s own permissions open the file and pass the descriptor to the privileged part.
Inherited Environment, Search Paths, and Library Injection
A program inherits environment variables, open descriptors, a working directory, and a file-creation mask from whatever started it, which may be the attacker. A search path such as PATH decides which program runs when a script names one, so a counterfeit placed earlier in the path runs first. The fixes are absolute paths and an environment the privileged program constructs itself.
Library injection makes a program load library code the attacker controls, and that code runs with the program’s authority. It takes two forms:
-
On Linux,
LD_LIBRARY_PATHchanges where the loader searches for libraries andLD_PRELOADnames libraries to load before all others. The loader ignores these variables for setuid programs, but ordinary programs remain exposed. -
Windows DLL loading is the same class of problem: a program that loads a library by name searches several directories, and a library placed where the search looks first is the one that runs.
In both cases, a library or program loaded by name from a location the attacker can influence is code the attacker chose.
Function interposition replaces or wraps one function that a program expected to get from a shared library. The loader resolves each function name to the first definition it finds, so a preloaded library that defines rand() or the clock function replaces it without modifying the program, and a wrapper can record a function’s arguments and then call the original. Interposition has legitimate uses in debugging and testing. In an attacker’s hands it captures data, changes arguments, or makes an authentication check return success.
Code That Arrives by Package Name
A package name resolves to code that runs with the installer’s privileges, and installing a package can run its install scripts before the package is ever imported.
Three attacks exploit the choice of which package a name resolves to:
-
Dependency confusion exploits a package manager that treats public and private repositories as one pool and picks the highest version, so a public package with an internal name wins.
-
Typosquatting registers a name that is a plausible misspelling of a popular package.
-
Nonexistent names suggested by AI assistants can be registered by an attacker before a developer installs them.
The defenses bind a name to a repository, run builds with few credentials and limited network access, and pin and review dependencies.
Containment
Operating system access control identifies users, not programs, so a compromised process holds every right of its user and the permission model cannot express a rule about one program.
The Principle of Least Privilege is a design principle: a program should run with the fewest permissions its job needs. Three ways to approach it fall short:
-
A dedicated account for the service still reaches every world-readable file.
-
Dropping privileges depends on the program’s own code.
-
Privilege separation depends on the program’s own code.
Containment enforces limits from outside the process. The limits hold after the process is taken over, apply to programs never written to restrict themselves, and allow code that was never trusted, such as apps, web scripts, and dependencies, to run at all. The environment that confines a program is a sandbox.
A confined process has to be kept from more than files:
-
Other processes.
-
The kernel’s system call interface.
-
The machine’s network identity.
-
Shared CPU, memory, disk, and process slots.
The kernel or hypervisor that enforces a boundary is part of the trusted computing base, since the boundary holds only if that layer is correct.
Every mechanism is judged by five questions:
-
What does it restrict?
-
Who enforces it?
-
What interface can the confined code still reach?
-
What overhead does it add?
-
How does it fail?
The second question places each mechanism at one of four levels: a user-level monitor process, a language runtime, the kernel, or the hypervisor. Each level trusts everything beneath it, and going down the list the enforcement gets stronger and the interface narrower.
chroot and Jails
chroot changes where path resolution starts for a process and its children. It restricts only names:
-
System calls, the network, and resource use are untouched.
-
A root process escapes.
-
A descriptor opened before the call stays open.
Jails added a private process view, a network address, and a restricted root.
The lessons are that hiding names is not the same as restricting operations, and that root inside a boundary has to be a weaker root than root outside.
System Call Filtering
The system call interface is the natural point for complete mediation, since a process acts on the world only through it.
Early sandboxes placed a monitor in a separate process, which had three problems:
-
The monitor had to replicate kernel state.
-
A second thread could change a file name between the monitor’s check and the kernel’s use.
-
Every call added two context switches.
seccomp-BPF moves the filter into the kernel. A process installs a filter that the kernel runs on every system call. The filter sees the call number and its integer arguments and allows the call, fails it, or kills the process.
The filter never reads process memory, so it cannot see a file name and cannot decide which files a process may open. Its purpose is to remove the kernel code behind unused calls from a process’s reach.
The overhead per call is small. The real work is a correct policy, which breaks the program if too strict and adds little if too loose.
AppArmor
AppArmor restricts a program by pathname: a profile attached to the program lists the files it may read, write, or execute, along with its network access.
The check runs inside the kernel after the path has been resolved, on the object actually being opened, so the race condition that defeats an external monitor does not apply.
The two kernel mechanisms divide the work: seccomp-BPF limits which system calls a process can make and cannot see names, while AppArmor allows the call and limits which objects it reaches.
Path-based rules are easier to write than label-based ones such as SELinux, but a file with two names can fall under two rules.
Language Runtime Sandboxes
A language runtime such as the Java virtual machine, a JavaScript engine, or WebAssembly confines a program from inside. It verifies code before running it, checks every index and type, and manages memory itself, so the program cannot form a pointer, read outside its memory, or make a system call. The program reaches the outside only through the library the runtime exposes.
The sandbox is only as correct as the runtime, which is a large program in C or C++. A memory bug in the runtime gives the program native code in the host process. A language sandbox needs an operating system boundary beneath it, which is how browsers are built.
Application Sandboxes and Permission Models
Phones make the application, rather than the user, the principal:
-
Android gives each app its own user ID and SELinux domain.
-
iOS gives each app its own container directory under a common sandbox profile, with extra abilities granted by signed entitlements.
Shared resources such as the camera and contacts are reached only through system services that check permissions before acting.
Permission models moved from install-time, all-or-nothing grants to requests at first use that the user can refuse or revoke, with finer grants such as approximate location and one-time access.
The model has three limits:
-
The user’s consent is the policy.
-
A permission covers a resource and not a purpose.
-
An app holding a permission can expose a component that lets another app use it, making the first app a confused deputy.
An escape needs a bug in the kernel or in a system service the app can reach.
Namespaces, Control Groups, and Capabilities
Three Linux kernel features confine groups of processes, and each controls a different thing:
-
A namespace gives a group of processes their own view of one part of the kernel’s state, such as process IDs, mounts, the network stack, or user IDs. Namespaces hide and partition; they change what a privileged call affects and do not forbid it.
-
A control group limits how much CPU, memory, and I/O a group of processes may consume and how many processes it may create, which ends the fork bomb as an attack.
-
Capabilities divide root’s authority into about forty separately grantable privileges, so a process can hold one without being root and root can run with most of them removed. One administrative capability covers so much that holding it is close to being root. These are unrelated to the capability lists of access control.
A user namespace maps root inside to an unprivileged user outside. That lets ordinary users create other namespaces, and it also lets them reach kernel code that once required root, a steady source of privilege-escalation bugs.
Containers
A container is a process started in new namespaces, placed in control groups, given reduced capabilities, run under a seccomp filter and an AppArmor or SELinux profile, with a root filesystem built from an image. The container runtime does the setup; the kernel itself does not know what a container is. Containers were built for packaging and deployment, and isolation was a side effect.
The benefits, at close to zero overhead:
-
Minimal images with fewer programs to attack.
-
Restrictive defaults.
-
Separation between services.
-
Fixes that ship by rebuilding an image.
A container escape gives code inside access to resources outside. The routes to one follow from the shared kernel:
-
Any kernel bug reachable from inside.
-
A flaw in the runtime, which runs on the host as root.
-
Misconfiguration, which is the most common failure: privileged containers, host mounts, the control socket, or root without a user namespace.
-
Public images, which are a supply chain.
Containers separate cooperating services well and are not, on their own, a boundary for hostile code. Rootless runtimes and running containers inside virtual machines reduce what an escape reaches.
Virtual Machines
A hypervisor, or virtual machine monitor, presents a complete computer to each guest operating system. The confined code is an entire operating system, and the interface it reaches is virtual hardware.
Three parts of the machine are handled differently:
-
Processor. With hardware support for virtualization, the processor runs guest instructions directly and hands control to the hypervisor only for operations that would affect the real machine. The processor itself is not emulated.
-
Memory. A second level of address translation, controlled by the hypervisor, limits a guest to its assigned memory.
-
Devices. A device is emulated by software in the hypervisor, supplied through a simplified interface written for virtual machines, or passed through to one guest.
A Type 1 hypervisor runs on the hardware. A Type 2 hypervisor runs under a host operating system, which then joins the trusted computing base.
Each guest has its own kernel, so a kernel bug in one guest stays in that guest, and the interface exposed to a guest is far narrower than a system call interface. A guest application’s system calls enter the guest kernel, not the hypervisor, which is the central difference from a container.
The overheads are transitions into the hypervisor, I/O through emulation, memory for a kernel per guest, boot time in seconds, and another operating system to patch.
The risks:
-
Escape through device emulation code.
-
Compromise of the hypervisor or its management services.
-
Leakage through shared hardware.
-
Sprawl.
Containers inside a virtual machine share that guest’s kernel, and an escape from the container still leaves the hypervisor boundary around the guest. Cloud providers separate customers this way.
Virtualization-Based Security
Virtualization-based security uses a hypervisor that starts before the operating system to protect parts of that operating system from its own kernel. A secure environment whose memory the normal kernel cannot touch decides which kernel code pages may execute and holds the credentials used to authenticate to other machines.
The limits:
-
The hypervisor and secure kernel join the trusted computing base.
-
A compromised kernel still reads every user file.
-
The configuration that turns it on is itself a target.
-
Signed older versions of the protected components can be reinstalled.
-
Code running before the hypervisor defeats it.
-
Compatibility problems and performance overhead lead people to turn it off.
Comparing the Mechanisms
The strength of a boundary depends on three things:
-
The size of the interface the confined code can reach.
-
The correctness of the layer that enforces it.
-
The correctness of its configuration.
A virtual machine has the smallest interface and strongest enforcement and adds the most overhead. A system call filter adds almost none and narrows the interface without closing it. Real systems layer them.
Every mechanism assumes the layer beneath it is honest: a filter trusts the kernel, a container trusts the kernel it shares, a virtual machine trusts the hypervisor, and the hypervisor trusts the firmware and the processor.
What You Don’t Need to Study
These exclusions apply to the command injection and containment material. Injection into web pages, prompt injection and AI security, and covert channels are not part of this material. We will cover these topics later.
The following are not required for the exam:
-
Incident names, dates, CVE numbers, versions, and victim counts, including the Zimbra, MOVEit, FortiSIEM, PHPMailer, BatBadBut, Log4Shell, PAN-OS, IIS, Nimda, Tomcat, NLWeb, Looney Tunables, Symbiote, Dirty Pipe,
runc, VENOM, ESXiArgs, BlackLotus, and Windows Downdate cases. Know the mechanisms described above and recognize them from a description. -
Which function in which language invokes a shell. Know the difference between passing a string to a shell and starting a program with an argument vector, and that
system(),popen(), andshell=Trueare the shell-invoking forms. -
The exact quoting, comment, and multi-statement rules of individual databases, or document-database operator syntax. Know that the rules differ and that a filter written for one interpreter is wrong for another.
-
The bit layout of overlong UTF-8 encodings, Windows path normalization, the Windows DLL search order, the parameters of
CreateProcess(), Windows command-line parsing, or the code in the interposition and descriptor-repair examples. Know the decode-before-validating rule, that a library or program loaded by name from a location the attacker controls is the attacker’s code, and what preloading a library can do. -
Package manager options and script names. Know what dependency confusion is and that installation runs code.
-
The history of Janus and Systrace, seccomp’s original mode, how filters are inherited, the BPF language, seccomp return codes, the supervisor notification feature, AppArmor’s profile syntax and modes, or the Linux Security Module framework.
-
How the browser is sandboxed. Know that it is privilege separation plus the mechanisms above, applied to a program that runs hostile code.
-
Android and iOS version numbers and the years particular permission changes shipped, or how entitlements are signed. Know the install-time to runtime progression and the limits of the model.
-
The complete list of namespace types, cgroup controller names, individual capability names, or the kernel versions that introduced each feature. Know what each of the three mechanisms controls and that user namespaces widen the kernel attack surface.
-
Container image layers, registries, port mapping, orchestration systems, or Docker commands.
-
Trap and emulate, the conditions a processor has to meet for it, binary translation, paravirtualization of the processor, VM exits, shadow page tables, IOMMUs, or the history of IBM, VMware, and Xen. Know what a hypervisor does, that guest code runs directly on the processor with hardware support, and that devices are emulated or passed through.
-
Windows’ names for VBS features or the two-environment architecture in detail. Know what VBS protects and its limits.