BugForge
All lessons
Node.js · 08/19 6 min

Reading request input safely

params, query and body: everything is a string until you parse it, and some of it is hostile.

Where input lives

Example Type you get
req.params /orders/:id string
req.query ?page=2&tag=a&tag=b string, array if repeated, object with the extended parser
req.body JSON body whatever JSON had, only if express.json() ran first
req.headers authorization string, keys lowercased

Strings that look like numbers

req.query.page + 1;           // "21"
Number("");                   // 0
Number("abc");                // NaN, and NaN comparisons are always false
parseInt("10abc", 10);        // 10, silently

Parse once, validate, then use the clean value:

function intParam(v, { def, min, max }) {
  if (v === undefined) return def;
  const n = Number(v);
  if (!Number.isInteger(n) || n < min || n > max) throw new BadRequest(`invalid number: ${v}`);
  return n;
}
const page = intParam(req.query.page, { def: 1, min: 1, max: 10_000 });
const limit = intParam(req.query.limit, { def: 20, min: 1, max: 100 });
const skip = (page - 1) * limit;   // page 1 → skip 0

Objects where you expected strings

With Express 4's default query parser, ?email[$ne]=x arrives as { $ne: "x" }. Passed straight into a Mongo filter, that matches every user.

if (typeof req.query.email !== "string") throw new BadRequest("email must be a string");

Same for search: escape user text before building a RegExp (q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), or a .* query returns everything and a ( throws.

Validate the body shape

  • Required fields present, correct types, sane lengths.
  • Allow-list fields you copy: const { name, price } = req.body, never Object.assign(doc, req.body) (that lets a client set role: "admin").
  • Normalise before comparing: email.trim().toLowerCase().
  • Bad input is 400 with a clear message, not a 500 from deep inside the model.
Next in Node.js
Auth: hashing, JWTs, 401 vs 403