Juicy Shop
Input Validation & Canonicalisation
Most injection and traversal bugs share a root cause: untrusted input is trusted too soon. Learn positive (allow-list) validation, canonicalisation, and why filename and path handling needs special care.
Validate positively, not negatively
Blocklists ask 'does this input look bad?' and always lose — attackers have endless encodings, cases, and tricks to slip past a list of forbidden patterns. Allow-lists ask 'is this input exactly one of the shapes I expect?' and reject everything else. Define the precise type, length, character set, and range each field may take, decode against that schema at the trust boundary, and discard anything that does not fit.
Canonicalise before you decide
Validation must happen on the final, canonical form of the data — not the raw string. '%2e%2e%2f', '..\', and '..%c0%af' can all decode to '../'. If you check the string before decoding, the check is meaningless. Always decode and normalise (resolve the path, lower-case the host, parse the URL) first, then validate the canonical result, then use that same canonical value.
Filenames and paths are dangerous
Joining a user-supplied name onto a base directory is a classic path-traversal sink: '../' sequences escape the directory and read arbitrary files. The safe pattern is to resolve the full path and confirm it is still prefixed by the base directory (plus a separator) before touching the filesystem. Better still, never accept raw names — map an opaque ID to a stored filename so the user never controls the path at all.
Validation is not output encoding
Input validation reduces the attack surface but does not make data safe for every sink. The same value may be safe in HTML yet dangerous in SQL, a shell, or a file path. Validate on the way in for shape and intent, and still apply the correct context-specific defence at each sink: parameterised queries for SQL, output encoding for HTML, and path canonicalisation for the filesystem.
Insecure vs secure
app.get("/file", (req, res) => {
// name = "../../../../etc/passwd"
const p = path.join(UPLOADS, req.query.name);
res.send(fs.readFileSync(p));
});The name is joined without canonicalisation, so '../' sequences escape UPLOADS and read arbitrary files.
app.get("/file", (req, res) => {
const full = path.resolve(UPLOADS, String(req.query.name));
if (!full.startsWith(UPLOADS + path.sep))
return res.status(400).end();
res.send(fs.readFileSync(full));
});The path is resolved to its canonical form and confirmed to stay inside UPLOADS before any file is read.
Key takeaways
- Prefer allow-lists (positive validation) over blocklists.
- Canonicalise/decode input first, then validate the canonical form.
- Resolve paths and confirm they stay inside the intended base directory.
- Map opaque IDs to filenames instead of accepting raw user paths.
- Validation complements — never replaces — sink-specific defences.
Knowledge check
Practice
Apply this lesson hands-on in the matching find-it / fix-it challenge.
Open challenge →