pk.org: Computer Security/Lecture Notes

Command Injection and Containment

Study Guide

Paul Krzyzanowski – 2026-10-23

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:

  1. An interpreter.

  2. Input the program does not control.

  3. 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:

The defenses fall into an order of preference:

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:

  1. Do not invoke a shell. Use a library call, or start the program directly with an argument vector (execve(), or subprocess.run() with a list), so that each argument arrives as one string that no shell parses.

  2. 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 ./.

  3. 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.

  4. 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:

Validating and Sanitizing Input

Three terms describe how input is handled before it reaches an interpreter:

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:

  1. Sanitization has to match the context of use, because SQL, a shell, HTML, and a file path have different special characters.

  2. Validating at a program’s entry points and trusting everything inside fails for the same reason second-order injection does.

  3. Checks in the browser are bypassed by sending the request directly.

  4. 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:

  1. Canonicalize with realpath() and check the prefix, including its trailing separator.

  2. Have the kernel enforce the restriction during resolution, so .. and links cannot leave a directory.

  3. Map an identifier to a file instead of accepting a path.

  4. 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 defenses share one rule, which is to operate on the object rather than on the name:

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:

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:

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:

  1. A dedicated account for the service still reaches every world-readable file.

  2. Dropping privileges depends on the program’s own code.

  3. 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:

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:

  1. What does it restrict?

  2. Who enforces it?

  3. What interface can the confined code still reach?

  4. What overhead does it add?

  5. 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:

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:

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:

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:

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 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:

A container escape gives code inside access to resources outside. The routes to one follow from the shared kernel:

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:

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:

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:

Comparing the Mechanisms

The strength of a boundary depends on three things:

  1. The size of the interface the confined code can reach.

  2. The correctness of the layer that enforces it.

  3. 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:

Next: Terms we covered