Skip to content

VOL 08 / CH 04 / LESSON 02

4.2 Server-Side Input Boundaries: Injection, Paths, and File Handling ​

Injection occurs when an interpreter cannot distinguish between "syntax written by the application" and "data provided externally." SQL is just one example; shell commands, templates, LDAP, log queries, and expression languages all share this same structural vulnerability.

Parameterized Queries Separate Values from Syntax ​

Dangerous code:

text
"SELECT * FROM users WHERE email = '" + email + "'"

The correct approach is to use database driver bind parameters:

sql
SELECT id, password_hash
FROM users
WHERE email = ?

Parameters represent only values and cannot alter query structure. Parameterized queries typically cannot bind table names, column names, or sort directions; these structural options should be handled by server-side enum mappings:

text
sort=recent → ORDER BY created_at DESC
sort=name   → ORDER BY display_name ASC

Never perform "escaping and concatenation" on user-supplied column names. Stored procedures that need dynamic SQL must also bind data values and constrain structural options; ORMs may reintroduce injection risks through raw queries, string filtering, or insecure escape mechanisms.

Database accounts should follow the principle of least privilege: read-only interfaces should not use schema owners, reporting tasks should not have write permissions, and a database identity serving one tenant should not read another tenant's schema. Shared identities serving multiple tenants need query constraints, row-level policies, or equivalent isolation. WAFs can block some known payloads, but they cannot replace proper separation of code and data.

This SQLite example checks binding in memory and uses no real accounts. Both a legitimate address containing a quote and an injection-shaped string are treated as complete email values; neither can change the WHERE condition:

python
import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (id INTEGER, email TEXT)")
db.execute("INSERT INTO users VALUES (?, ?)", (1, "alice@example.com"))

def find_user(email):
    return db.execute("SELECT id FROM users WHERE email = ?", (email,)).fetchall()

assert find_user("alice@example.com") == [(1,)]
assert find_user("' OR 1=1 --") == []
assert find_user("o'connor@example.com") == []
db.close()

Shell Commands Should Not Go Through the Shell ​

Danger:

text
shell("convert " + uploadedPath + " output.png")

Instead, prefer using library APIs. When process execution is unavoidable, use a parameter array and disable shell parsing:

text
execve("/usr/bin/convert", ["convert", validatedInput, outputPath], cleanEnv)

A parameter array reduces the risk of command separator injection, but does not automatically ensure the safety of arguments passed to the target program. Filenames starting with - may be interpreted as options; some tools support reading additional input from URLs, configuration files, or response files. Use -- to end options when the target tool supports it, along with server-generated paths, an explicit allowlist, and a low-privilege sandbox. Not every program supports --; adding it blindly is not a general defense.

Templates Must Distinguish Between Presentation and Code Templates ​

Treating user input as template data can usually be safely escaped. However, allowing users to control the template source code introduces the risk of Server-Side Template Injection, which could allow reading of process objects, environment variables, or even execution of arbitrary commands.

text
render(trustedTemplate, { displayName: untrustedValue })  ✓
render(untrustedTemplateSource, model)                    High risk

When user-controlled templates are absolutely necessary, use a restricted template language designed for untrusted input, a fixed set of safe functions, resource limits, and process isolation. Simply removing a few dangerous keywords is insufficient to establish a secure sandbox.

Path normalization cannot rely on string prefixes ​

Common issues with download interfaces:

text
/download?file=../../etc/passwd

Security design should prioritize having clients submit opaque object IDs that are then mapped to storage objects by the server database, rather than accepting filesystem paths directly. If relative paths must be processed, the following steps should be taken:

  1. Decode the input once according to an explicit rule and reject null bytes and invalid formats;
  2. Resolve the canonical path under a fixed root directory;
  3. Verify that the resulting path remains within the root directory, paying attention to path separators and case sensitivity rules;
  4. Prevent symlink traversal and replacement between checking and opening the file (TOCTOU);
  5. Use directory file descriptors and APIs that constrain the whole resolution process, such as Linux openat2 with RESOLVE_BENEATH/RESOLVE_NO_SYMLINKS. O_NOFOLLOW on the final component alone does not stop symlinks or replacements in intermediate directories.

A string-prefix check such as startsWith(root) incorrectly accepts /srv/uploads-evil as a descendant of /srv/uploads.

File Upload is a Content Processing Pipeline ​

File extensions and browser Content-Type are controlled by the client. The upload service must enforce:

  • Limits on file size, quantity, compression ratio, and processing duration;
  • Checks for allowed MIME types via magic bytes and full parsing;
  • Server-generated object names stored outside the web root or in a separate object store;
  • Secure Content-Type, Content-Disposition, and nosniff settings when serving files for download;
  • Encoding and transcoding of images, documents, and media in isolated, low-privilege sandboxes with constrained resources;
  • Malware scanning as an additional safeguard, not as the sole security boundary;
  • Use of a separate origin for public content to prevent uploaded HTML from gaining access to the main site’s privileges.

Zip slip, zip bombs, and parser vulnerabilities demonstrate that "storing files without executing them" is not equivalent to security.

Untrusted Deserialization Can Restore Behavior ​

Some object serialization formats can specify types, constructors, or gadget chains. Do not deserialize untrusted native object graphs from sources such as cookies, queues, caches, or uploaded files. Prefer schema-based formats that explicitly represent data, restrict field and type definitions, and enforce limits on depth, length, and nested structures.

A signature only proves that data originated from a party holding the key; if multiple low-trust services share the same signing key, any one of them could construct a dangerous object. Secure formats and minimal parsing capabilities cannot be substituted by signatures alone.

Exceptional Paths Must Also Maintain Boundaries ​

When parsing fails, do not fall back to a "compatibility mode" that re-executes the original string. Never return database errors, command stderr, absolute paths, or stack traces to external users. Log an internal correlation ID, provide the client with a consistent error model, and monitor for abnormal exception rates.

The next lesson asks whose authority a browser or server uses when it sends a request.

OWASP Top 10:2025 lists "Mishandling of Exceptional Conditions" as its standalone A10 entry, emphasizing that security design must not rely solely on handling successful paths.

References ​

Built with VitePress | Software Systems Atlas