Capstone: Harden This Express App

Advanced
16 min

Capstone: Harden This Express App

This final chapter puts the whole course to work. Below is a small notes API of the kind found in countless tutorials. It works, its tests pass, and it contains a dozen flaws from earlier lessons. Your task is to find them, map each to its lesson, and arrive at the hardened version in the sample code.

Treat it as a real review: list findings with severity, fix them, then prove it.

The Starting Point

javascript
const express = require("express"); const jwt = require("jsonwebtoken"); const { exec } = require("node:child_process"); const SECRET = "notes-app-secret"; // (1) const app = express(); app.use(express.json()); // (2) app.post("/login", async (req, res) => { const user = await User.findOne({ email: req.body.email, password: req.body.password }); // (3)(4) if (!user) return res.status(401).send("No user with that email"); // (5) res.cookie("token", jwt.sign({ id: user._id, role: user.role }, SECRET)); // (6)(7) res.send("ok"); }); app.get("/notes/:id", async (req, res) => { res.json(await Note.findById(req.params.id)); // (8) }); app.post("/notes", async (req, res) => { const user = jwt.decode(req.cookies.token); // (9) res.json(await Note.create({ ...req.body, ownerId: user.id })); // (10) }); app.get("/export", (req, res) => { exec(`zip -r /tmp/${req.query.name}.zip /data/notes`, () => res.download(`/tmp/${req.query.name}.zip`)); // (11) }); app.use((err, req, res, next) => res.status(500).send(err.stack)); // (12) app.listen(3000);

The Findings

| # | Finding | Severity | Lesson | |---|---|---|---| | 1 | Hard-coded signing secret | High | Secrets Management | | 2 | No body limit, helmet or rate limit | Medium | Secure Coding in Node.js | | 3 | Plain-text password compared in the query | Critical | Passwords and Hashing | | 4 | Raw body objects in the query (operator injection) | High | Injection Beyond SQL | | 5 | Login error reveals whether the email exists | Low | Authentication Failures | | 6 | Cookie without HttpOnly, Secure, SameSite | High | Cookies and Sessions | | 7 | JWT with no expiry; algorithm not pinned | High | JWT Pitfalls | | 8 | Any user can read any note by ID | Critical | Broken Access Control | | 9 | jwt.decode trusts an unverified token | Critical | JWT Pitfalls | | 10 | Mass assignment: body may set ownerId | High | Input Validation | | 11 | Shell command and path built from user input | Critical | Injection Beyond SQL | | 12 | Stack traces returned to clients | Medium | Security Misconfiguration |

The Fixes

The sample code is the result: keys from a validated config module; helmet, a bounded parser and a rate limiter as the baseline; ownerId: req.user.id in every note query; bodies through a strict schema with the owner taken from the session; the export feature removed until it can be built without a shell. The authentication module handles the rest:

javascript
// auth.js (excerpt): verified token, pinned algorithm, hardened cookie const payload = jwt.verify(token, config.JWT_PUBLIC_KEY, { algorithms: ["RS256"], issuer: config.ISSUER }); res.cookie("token", signed, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 15 * 60_000 });

Login hashes with Argon2id, verifies a dummy hash for unknown users, and returns one generic message; the error handler logs internally and answers generically.

Proving It

Run the checks from the testing lesson.

bash
npm audit --audit-level=high # dependencies semgrep scan --config p/nodejs --config p/express # the exec() finding disappears npm test # 404 for foreign notes, 400 for operator objects docker run --rm ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t https://staging.example.com

If every finding has a test or scan behind it, the app is ready.

Quick Quiz
Question 1 of 2

Finding 8 lets any authenticated user read any note. What is the correct fix?

Key Takeaways

  • A working application with passing tests can still contain a dozen serious flaws; security review is its own activity.
  • Most fixes are structural: ownership in queries, boundary validation, secrets from a store, a middleware baseline.
  • Every fix needs a test or a scan that would fail if the flaw returned.
  • The course checklists (threat model, deny by default, hardening, dependency hygiene) are reusable on every project.

What to learn next: the Express.js and REST APIs courses for backend hardening in context, the Docker and Linux courses for runtime and host security, then deliberately vulnerable training applications and capture-the-flag exercises on systems you are authorized to test.

Capstone: Harden This Express App - Cyber Security | CodeYourCraft | CodeYourCraft