Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

This endpoint has auth. The tests are green. It still lets a user hand themselves admin.

Here's a pattern I keep running into in AI-scaffolded update endpoints: the code looks completely ordinary. Nothing about it reads as risky. The whole endpoint is one line most people never look twice at: app.patch('

Here's a pattern I keep running into in AI-scaffolded update endpoints: the code looks completely ordinary. Nothing about it reads as risky.

The whole endpoint is one line most people never look twice at:

app.patch('/api/users/:id', requireAuth, async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
  res.json(user);
});

requireAuth checks that someone is logged in. It never checks what they're allowed to send. req.body goes straight into the update, whatever shape it happens to be.

Where this breaks

The intended use is updating your own name, bio, or avatar URL. Normal, boring, exactly what AI writes for you in a second when you ask for "let users edit their profile."

Nothing stops the client from sending fields nobody meant to expose:

curl -X PATCH https://yourapp.com/api/users/YOUR_OWN_ID \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Same As Before","role":"admin"}'

If role is a column on that user row, that request just promoted the caller to admin. Nothing crashed. The response looks exactly like a normal profile update, because as far as the database is concerned, it was one.

I shipped a version of this myself, and I didn't catch it right away. Every tool I ran on it was happy — linter clean, tests green, and an AI reviewer signed off. None of them asked what the client was allowed to put in that body.

Why this slips past both AI and manual review

The AI wired up exactly what was asked: read the request body and write it straight into the record. Nobody told it that role, is_admin, credits, or plan_id are different in kind from name and bio. It has no way to know a column is privileged unless something tells it so.

Manual testing has the same blind spot, because you're the one writing every request by hand. You check that your own update works, and you have no reason to try sending a field you were never supposed to touch.

Check your own project in two minutes

Search your codebase for anywhere a request body gets passed into a write call without being filtered first:

grep -rn "findByIdAndUpdate(req\|\.update(req\.body\|\.create(req\.body" .

For every hit, ask: does the model behind this call have any column a normal user shouldn't be able to set themselves? Role, admin flags, credit balances, subscription tier, ownership fields — anything like that sitting in the same table as the profile fields users are supposed to edit is a mass assignment waiting to happen.

The fix

Never pass req.body into a write call directly. Pick only the fields you meant to allow:

app.patch('/api/users/:id', requireAuth, async (req, res) => {
  const { name, bio, avatarUrl } = req.body;
  const user = await User.findByIdAndUpdate(
    req.params.id,
    { name, bio, avatarUrl },
    { new: true }
  );
  res.json(user);
});

Now role in the request body just gets ignored, no matter who sends it. If you're using a schema validator like Zod, .pick() or .strict() on the update shape does the same thing with less to maintain by hand — and it fails loudly on an unexpected field instead of silently dropping it.

If a privileged field genuinely needs to change through an API, give it its own endpoint with its own authorization check, separate from the one a regular user hits to edit their bio.

The habit that catches this

Every time a request body flows into a write, it's worth asking one question before shipping: what's allowed in here, and did I decide that on purpose, or did the tool decide it for me? Most of the checks I run don't ask that at all. It's the one I've started asking by hand.

I put together a small tool that checks a repo for this pattern and a few related ones automatically. If you want me to run it against yours for free, drop a comment.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.