Here is an example of Node.js code that is vulnerable to mass assignment:
🥺 Vulnerable Code
// Vulnerable: the entire request body is trusted and written to the model
app.post("/api/users", async (req, res) => {
const user = await User.create(req.body);
res.json(user);
});
app.patch("/api/users/:id/profile", async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(user);
});The UI only sends displayName and bio, but nothing stops a client from adding "role":"admin", "emailVerified":true, or "credits":99999 to the same JSON body. The update route is worse: it takes the user id from the URL, so one authenticated user can rewrite another user's record.
😎 Secure Code
Here is a version of the same code that is secured against mass assignment:
const ALLOWED_PROFILE_FIELDS = ["displayName", "bio", "avatarUrl"];
function pick(source, allowed) {
return Object.fromEntries(
Object.entries(source).filter(([key]) => allowed.includes(key))
);
}
app.patch("/api/users/:id/profile", requireAuth, async (req, res) => {
if (req.params.id !== String(req.user.id)) {
return res.status(403).json({ error: "Forbidden" });
}
const updates = pick(req.body, ALLOWED_PROFILE_FIELDS);
const user = await User.findByIdAndUpdate(req.user.id, { $set: updates }, { new: true });
res.json({ id: user.id, displayName: user.displayName, bio: user.bio });
});Only the three fields a profile form is allowed to change survive the allowlist, the write is scoped to the authenticated user rather than the id in the URL, and the response is an explicit shape instead of the raw document. Privileged fields such as role and credits get their own admin-only endpoints with their own authorization checks.