100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Web Application Security
60 minintermediate

Practice — Securing a REST API with Rate Limits and Schema Validation

What You'll Build

In this lab you will take an insecure REST API and harden it against the Module 4 risks: you will add object level authorization, an input schema that stops mass assignment, an output projection that stops data exposure, and rate limiting on a sensitive endpoint. You will finish with tests that prove each control works.

Analogy🏏Cricket
💪 Think of it like fitness: Assessment day is when the training gets tested for real, you take the map of your programme and put each targeted lift under load to see which strengths hold and which weaknesses are genuine, recording every number. Just as the measured test turns a plan into proven results, using your attack-surface map to probe injection, authentication, and API authorization, and confirming each real flaw with captured evidence, turns recon into demonstrated findings. This reveals where knowledge becomes proof: you deliberately load each mapped weakness and record exactly what gives way.

The aim is to convert the checklist into muscle memory by applying every control to one small, coherent API. Rather than studying the risks separately, you will see how object authorization, schema validation, output projection, and rate limiting fit together on the same endpoints, which is exactly how they must combine in real services.

Analogy🏏Cricket
🏏 Think of it like cricket: A disciplined bowling attack does not spray deliveries everywhere and hope; guided by the scouting report, it targets each batter's flagged weakness, confirms the ball is doing what was planned, and records every dismissal for the analysts. Just as that planned, evidence-backed attack turns a dossier into wickets rather than wasted overs, working your test plan area by area, confirming each real flaw by safely triggering it, and documenting it rigorously turns recon into evidenced findings. This reveals the method: targeted, confirmed, and recorded beats spray-and-pray every time.

Prerequisites

  • Node.js 18+ and npm installed, plus a terminal for running commands.
  • Completion of the Module 4 readings on the API Top 10, rate limiting, and mass assignment, whose controls you will apply directly.
  • Basic familiarity with Express routes and JSON request and response bodies.
  • An understanding that these techniques apply to this local lab or APIs you are explicitly authorised to test only.

Setup & Project Structure

Create a small Express API with a users resource that is deliberately insecure: it authorises only by login, binds the whole request body on update, returns raw user objects, and has no rate limiting. Seed a couple of users so you can attempt cross-user access. Run it locally and keep an HTTP client handy to send crafted requests throughout.

Analogy🏏Cricket
📷 Think of it like photography: A professional does not trust memory on a shoot; every frame is captured with its settings, the aperture, the shutter, the exact scene, so the shot can be reproduced and proven later. Just as that per-frame metadata makes each image a self-contained, repeatable record, working your test plan area by area and capturing the exact request, the response that proves impact, the reproduction steps, and the affected endpoint makes each finding a self-contained record. This reveals the discipline: evidence recorded at the moment of capture is what makes a finding stand up in the report.
bash
# Scaffold the insecure API
mkdir secure-rest-lab && cd secure-rest-lab
npm init -y
npm install express express-rate-limit zod
# In app.js: GET/PATCH /api/users/:id with NO object authorization,
# whole-body binding on PATCH, raw object responses, no rate limits.
node app.js          # serves on http://localhost:3000

Step 1 — Foundation: Confirm the Weaknesses

Before fixing anything, demonstrate each flaw against the running API. Log in as one user and read another user's record by changing the id, confirming broken object level authorization and excessive data exposure in one call. Then send a profile update that includes an isAdmin field to confirm mass assignment. Record each request and the response that proves the weakness.

Analogy🏏Cricket
💪 Think of it like fitness: A good coach films your squat and deadlift before writing any program, exposing each weakness, the collapsing knee, the rounded back, on camera so the plan can target what is genuinely wrong. Just as the coach demonstrates and records every fault before correcting a single one, you exploit and record each API flaw, the cross-user read and the isAdmin escalation, before patching anything at all. This reveals why you probe before you fix: a correction aimed at a weakness you have actually witnessed is one you can later prove, on film, that you have truly resolved.
bash
# Confirm BOLA + excessive exposure: read another user's record
curl -s http://localhost:3000/api/users/2 -H "Authorization: Bearer <user1-token>"
# Returns user 2's full record incl. passwordHash — two flaws at once.

# Confirm mass assignment: escalate via an unexpected field
curl -s -X PATCH http://localhost:3000/api/users/1 \
  -H "Authorization: Bearer <user1-token>" -H "Content-Type: application/json" \
  -d '{"displayName":"Alice","isAdmin":true}'
# The isAdmin field is bound and set — privilege escalation.

Step 2 — Core Logic: Add Authorization and Schema Validation

Now apply the two central controls. Scope every object access to the authenticated caller so reading another user's record returns 404, closing broken object level authorization. Add a strict input schema on update that accepts only the fields a user may change and rejects unknown fields, closing mass assignment. Re-run your Step 1 requests to see both attacks now fail.

Analogy🏏Cricket
🍳 Think of it like cooking: When a dish is failing, a chef first corrects the two faults that ruin it most, the seasoning and the cooking time, before fussing over any garnish, because those two decide whether the plate is even edible. Just as fixing the highest-impact faults first rescues the dish, adding object level authorization and a strict input schema first closes the two flaws an attacker reaches most easily and most damagingly. This reveals a sound order of work: tackle the controls that stop the worst harm first, then refine, and taste again, by re-running the attacks, to confirm they now fail.
javascript
// Object level authorization: scope to the caller
app.get('/api/users/:id', requireAuth, async (req, res) => {
  if (String(req.user.id) !== req.params.id) return res.status(404).end();
  const user = await db.users.findById(req.params.id);
  res.json(user);   // output projection added in Step 3
});

// Strict input schema stops mass assignment
const UpdateUser = z.object({
  displayName: z.string().max(80),
  bio: z.string().max(500).optional(),
}).strict();                          // unknown fields (isAdmin) rejected
app.patch('/api/users/:id', requireAuth, async (req, res) => {
  if (String(req.user.id) !== req.params.id) return res.status(404).end();
  const data = UpdateUser.parse(req.body);   // throws on isAdmin
  res.json(await db.users.update(req.params.id, data));
});

Step 3 — Integration & Enhancement: Output Projection and Rate Limits

Next, close the remaining two gaps. Add an output projection so responses carry only safe fields, ensuring the password hash and internal flags never leave the server even on the user's own record. Then add a strict rate limiter to the login endpoint, keyed to the account, so credentials cannot be brute-forced. Verify the projection hides sensitive fields and the limiter returns 429 after the threshold.

Analogy🏏Cricket
💰 Think of it like finance: Having secured the vault itself, a bank still redacts the sensitive numbers on every statement it mails out and caps how many times a PIN may be tried before the card locks, closing the quieter ways money and data slip away. Just as redaction and attempt limits finish the bank's defences, an output projection that strips the password hash and a login rate limiter returning 429 finish the API's hardening. This reveals why the later controls still count for so much: once the obvious doors are shut, it is the leaking statement and the endless PIN guess that an attacker turns to next.
javascript
// Output projection: never emit sensitive fields
function publicUser(u) {
  return { id: u.id, displayName: u.displayName, bio: u.bio };  // safe set
}
// Apply publicUser() to every user response above.

// Strict, account-keyed rate limit on login
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, max: 5,
  keyGenerator: req => req.body.email || req.ip,
  standardHeaders: true,
});
app.post('/login', loginLimiter, handleLogin);   // 429 after 5 tries

Step 4 — Testing & Verification: Prove Every Control

Finally, lock all four controls in with automated tests so a future change cannot silently regress them. Assert that reading another user's record returns 404, that an update containing isAdmin is rejected, that responses never contain the password hash, and that the sixth rapid login attempt returns 429. A green suite is your evidence that the API meets the Module 4 checklist.

Analogy🏏Cricket
⚽ Think of it like sports: Modern matches lean on automated goal-line technology and replay review so a decision cannot quietly go wrong unnoticed; the system flags the instant a rule is breached, every single game, without relying on anyone's memory. Just as automated officiating guards each rule consistently match after match, an automated test suite guards each API control, the 404, the rejected isAdmin, the hidden hash, and the 429, run after run. This reveals why the tests are the real deliverable here: like replay review, they turn 'we think it is correct' into standing, repeatable proof that no future change has quietly broken the rules.
javascript
// verify.test.js — one test per control
test('BOLA closed: other user record is 404', async () => {
  expect((await get('/api/users/2', user1)).status).toBe(404);
});
test('mass assignment rejected: isAdmin refused', async () => {
  const r = await patch('/api/users/1', user1, { displayName:'A', isAdmin:true });
  expect(r.status).toBe(400);
});
test('no data exposure: response omits passwordHash', async () => {
  const r = await get('/api/users/1', user1);
  expect(r.body).not.toHaveProperty('passwordHash');
});
test('rate limit: 6th login returns 429', async () => {
  for (let i = 0; i < 5; i++) await post('/login', { email, pass: 'x' });
  expect((await post('/login', { email, pass: 'x' })).status).toBe(429);
});

Warning: Run this lab only against your own local API. The crafted requests here are safe on your machine but constitute unauthorised access and testing if aimed at any system you do not own or have explicit written permission to test. Keep the server on localhost and never point these requests at a live third-party service.

Extension Challenge: Extend the lab three ways. Add function level authorization by introducing an admin-only endpoint and proving a normal user gets 403, covering the second-biggest API risk. Move the rate-limit counter into Redis and confirm the limit holds when you run two server instances. Finally, add a response schema (not just a request schema) so a newly added internal field cannot leak, demonstrating the output-boundary discipline from Lesson 23.

  • Real API hardening combines controls on the same endpoints: object authorization, input schema, output projection, and rate limiting working together.
  • Reading another user's record by changing the id demonstrates broken object level authorization and excessive data exposure in a single request.
  • A strict input schema that rejects unknown fields closes mass assignment, so an added isAdmin field is refused rather than bound.
  • Scoping every object access to the authenticated caller and returning 404 for others' records closes broken object level authorization.
  • An output projection ensures sensitive fields like the password hash never leave the server, even on the user's own record.
  • Automated tests — one per control — prove the API meets the checklist and guard against a future change silently regressing any control.
Lesson 24 of 35
0% complete