<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Untrusted Input

Untrusted Input

Every value that comes from outside your code (a form field, a URL parameter, a header, an uploaded filename) is untrusted. The entire family of injection attacks comes from one mistake: letting that untrusted data get interpreted as code instead of staying data.

It helps to separate three things people lump together as "sanitizing input":

  • Validation: reject input that doesn't fit the expected shape (an email that isn't an email, a quantity that isn't a positive number). Do it at the boundary, but it's a filter, not a security guarantee.
  • Sanitization: strip or neutralize dangerous content from input you intend to keep.
  • Escaping / parameterizing: the real fix. Keep data and code in separate lanes so the data can never be parsed as a command, no matter what it contains.

The principle below appears three times, in three different interpreters: a SQL database, an HTML renderer, and a filesystem. Same bug, same fix.

SQL Injection

A SQL injection happens when user input is concatenated into a query string and the database parses part of it as SQL:

// DON'T: the input becomes part of the SQL command
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// email = "' OR '1'='1"        → returns every user
// email = "'; DROP TABLE users; --"  → exactly what it looks like

The fix is a parameterized query (a prepared statement). You send the SQL and the values separately; the driver guarantees the values are only ever treated as data:

// DO: the $1 placeholder is filled with `email` as a pure value, never parsed as SQL
db.query("SELECT * FROM users WHERE email = $1", [email]);

There's no clever escaping to get right by hand. Parameterized queries are the answer, and every database driver and ORM supports them.

XKCD 327: A school calls a mom to say they had trouble with their computer. She asks if she really named her son Robert'); DROP TABLE Students;-- and the school confirms they've lost this year's student records. She says she hopes they've learned to sanitize their database inputs.
xkcd 327: "Exploits of a Mom"

HTML Injection (XSS)

The same bug in the browser is cross-site scripting (XSS) : untrusted input rendered into a page where the browser parses it as HTML and runs any script it contains, in the victim's session, with their cookies.

// DON'T: input is parsed as markup, so a <script> or onerror handler runs
element.innerHTML = `<p>${comment}</p>`;
// comment = "<img src=x onerror=alert(document.cookie)>"  → runs in the victim's browser

// DO: render it as text, so the browser shows the characters literally
element.textContent = comment;

Modern frameworks escape by default (React's <p>{comment}</p> is safe), but every framework also has an escape hatch (dangerouslySetInnerHTML, v-html, [innerHTML]) that re-opens the hole the moment you feed it untrusted data. A second layer, a Content-Security-Policy header, limits what scripts can run even if something slips through:

res.setHeader("Content-Security-Policy", "default-src 'self'");

Directory Traversal

When a filename from the user is used to build a filesystem path, ../ lets the request climb out of the intended folder: a directory traversal :

GET /files/../../etc/passwd     → reads a system file, far outside your uploads folder

The fix is to resolve the final path and confirm it's still inside the directory you meant to serve:

import path from "node:path";

const ROOT = "/var/www/files";

app.get("/files/:name", (req, res) => {
  const target = path.resolve(ROOT, req.params.name);   // collapses any ../ segments
  if (!target.startsWith(ROOT + path.sep)) {
    return res.status(403).end();                        // escaped the root, reject
  }
  res.sendFile(target);
});

Validating the input for .. is fragile: encodings and edge cases slip past. Resolving the path and checking the result stays inside the root is robust, because it verifies the actual outcome. Note that path.resolve collapses ../ segments but does not follow symbolic links. In high-security contexts where symlinks inside the root could point outside it, use fs.realpath() instead to fully resolve the path before checking the boundary.

Check your understanding

Question 1 of 2

Why isn't stripping quotes and semicolons enough to stop SQL injection?

A teammate "fixes" SQL injection by stripping quotes and semicolons from input before building the query string. Why isn't that enough?