// ───────────────────────────────────────────────────────────────────────────── // A05:2025 INJECTION (2021: A03 same name, down from #3; includes XSS) // ───────────────────────────────────────────────────────────────────────────── // WHAT : Untrusted data is sent to an interpreter as part of a command or // query, so data becomes code. // HAPPENS : SQLi (string concat into SQL); NoSQLi ({ $gt: '' } from JSON body // into Mongo); command injection (exec('ping ' + host)); XSS — // reflected / stored / DOM (innerHTML, document.write, eval); // SSTI (user string compiled as template); LDAP / XPath injection; // header injection (CRLF in redirect Location / Set-Cookie); log // injection; ORM injection via raw(); GraphQL injection. // SPOT : template literals or + building SQL/shell/HTML; `exec(`; `$where`; // req.body passed straight into find(); innerHTML / dangerouslySet // InnerHTML / v-html; `new Function`; `res.redirect(req.query.next)`; // Handlebars.compile(userInput). // FIX : parameterized queries / prepared statements ($1, ?); ORM query // builders; schema validation (zod/joi) rejecting objects where // strings expected; execFile with argv array (no shell); contextual // output encoding (escapeHtml, attr, JS, URL); textContent not // innerHTML; sanitizer (DOMPurify) for rich text; CSP as defense in // depth; least-privilege DB user; allowlists over denylists. // SAY : CWE-89 SQL Injection, CWE-943 NoSQL Injection, CWE-78 OS Command // Injection, CWE-79 XSS, CWE-94 Code Injection, CWE-1336 SSTI, // CWE-113 HTTP Response Splitting. Pitch: "Separate code from data: // parameterize, validate on input, encode on output, CSP as backstop." // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── 1. SQL injection // VULNERABLE: // const sql = `SELECT * FROM users WHERE email = '${email}' AND pw = '${pw}'`; // db.query(sql); // email = "' OR 1=1 --" // FIXED: query text is constant; values travel separately as parameters. function buildVulnerableSql(email) { // shown ONLY to demonstrate; never executed return `SELECT * FROM users WHERE email = '${email}'`; } function buildSafeQuery(email) { return { text: 'SELECT * FROM users WHERE email = $1', values: [email] }; } const payload = "' OR 1=1 --"; assert.equal(buildVulnerableSql(payload), "SELECT * FROM users WHERE email = '' OR 1=1 --'"); // the WHERE is gone const q = buildSafeQuery(payload); assert.equal(q.text, 'SELECT * FROM users WHERE email = $1'); // text unchanged by input assert.deepEqual(q.values, ["' OR 1=1 --"]); // payload is just a string value assert.equal(q.text.includes(payload), false); // ── 2. NoSQL injection // VULNERABLE: // const user = await User.findOne({ email: req.body.email, password: req.body.password }); // body = { "email": "admin@x.com", "password": { "$gt": "" } } -> matches any password // FIXED: validate types at the boundary; reject operator keys. function requireString(v, field) { if (typeof v !== 'string') throw new TypeError(`${field} must be a string`); if (v.length === 0 || v.length > 256) throw new RangeError(`${field} length`); return v; } function loginFilter(body) { return { email: requireString(body.email, 'email'), password: requireString(body.password, 'password') }; } assert.deepEqual(loginFilter({ email: 'a@b.c', password: 'hunter2' }), { email: 'a@b.c', password: 'hunter2' }); assert.throws(() => loginFilter({ email: 'admin@x.com', password: { $gt: '' } }), /password must be a string/); assert.throws(() => loginFilter({ email: { $ne: null }, password: 'x' }), /email must be a string/); function hasOperatorKey(obj) { if (obj === null || typeof obj !== 'object') return false; return Object.keys(obj).some((k) => k.startsWith('$') || hasOperatorKey(obj[k])); } assert.equal(hasOperatorKey({ a: { b: { $where: 'this.x' } } }), true); assert.equal(hasOperatorKey({ a: 'plain' }), false); // ── 3. OS command injection // VULNERABLE: // exec(`ping -c 1 ${req.query.host}`); // host = "8.8.8.8; rm -rf /" (shell parses it) // FIXED: execFile with an argv array — no shell, so metacharacters are inert. Validate anyway. // const { execFile } = require('node:child_process'); // execFile('ping', ['-c', '1', host], { timeout: 5000 }, cb); const HOSTNAME = /^[A-Za-z0-9.-]{1,253}$/; function pingArgs(host) { if (!HOSTNAME.test(host)) throw new Error('invalid host'); return ['-c', '1', host]; } assert.deepEqual(pingArgs('example.com'), ['-c', '1', 'example.com']); assert.throws(() => pingArgs('8.8.8.8; rm -rf /'), /invalid host/); assert.throws(() => pingArgs('$(whoami)'), /invalid host/); assert.throws(() => pingArgs('a.com && cat /etc/passwd'), /invalid host/); // ── 4. XSS — output encoding // VULNERABLE: // res.send(`