On 2 November I posted the first free "bug of the week": a small task app, one bug, solved in the browser. Here is how it worked, why the obvious fix is the wrong one, and what bug #2 is.

The setup

Tasklane is a fictional task app for small teams. You are signed in as Sam, a regular member.

  • GET /api/me returns your profile, including "role": "member".
  • The profile form lets you change one thing: your display name.
  • GET /api/admin/users answers 403 admin_only.

The goal was to get that admin list as Sam.

The bug, in three requests

1. Read the baseline. GET /api/me shows more fields than the form does: id, name, email, role, team. That is the hint. The form only sends name, but the record has a role.

2. Send a field the form never sends.

PATCH /api/me Content-Type: application/json { "name": "Sam Lee", "role": "admin" } 

The response comes back with "role": "admin". The server saved it.

3. Check the impact. GET /api/admin/users now answers 200 with every user in the workspace.

Why it worked

This is the handler:

app.patch("/api/me", requireLogin, async (req, res) => { const changes = req.body; const user = await db.users.update(req.user.id, changes); res.json(user); }); 

It saves the whole request body into the user record. So the client decides which columns change, and role is one of them. The form never offered that field, but the form is not the API.

The name for this is mass assignment. OWASP lists it as Broken Object Property Level Authorization (API3:2023).

The fix, and two that look like fixes

The case offered three one-line replacements.

Hide the role input in the form. Does nothing. The attacker edits the request, not the form.

Strip role from the body (a denylist):

const { role, ...changes } = req.body; 

This blocks today's attack and fails next month, when someone adds a plan or is_verified column and forgets to extend the list.

Copy only what a user may edit (an allowlist):

const changes = { name: req.body.name }; 

This is the right one. New columns are ignored by default, so a forgotten field fails closed instead of open.

The re-test

A fix is not done until the attack is replayed. With the allowlist in place:

  • PATCH /api/me with role: "admin": the role is ignored, you stay a member.
  • GET /api/admin/users: 403 again.
  • PATCH /api/me with a new name: still works.

The third check matters as much as the first two. A fix that breaks the normal feature gets reverted.

This is not a textbook-only bug

In 2012 the same class of bug let a researcher add his own key to the Rails organisation on GitHub, through a form that accepted a field it should not have. GitHub fixed it in about an hour.

Bug of the week #2 is live

Roastbox is a fictional shop that sells coffee gear. You are signed in as Nora, a normal customer. The grinder costs 49.00.

Can you pay less?

Same rules as last week: it runs in your browser against a simulated API (*.example.test), no account, about 10 minutes, and you finish by picking the fix and replaying the attack.

Try it: https://breachloom.com/?utm_source=devto&utm_medium=article&utm_campaign=weekly-w46#/weekly

I'll post the write-up next Monday with bug #3. If you solve it, tell me how long it took, but keep the answer out of the comments for now.