Authorization
Protect object properties
Allowlist fields callers may read or change so mass assignment and excessive data exposure cannot cross property-level permissions.
A user may access an object without being allowed to see or edit every property on it. Object-level authorization answers “may you touch this invoice?”. Property-level authorization answers a second question: “which fields of it?”.
Allowlist input fields
Parse request bodies into purpose-specific input types. Do not spread arbitrary JSON into a database update:
// mass assignment: everything in the body reaches the database
await db.invoices.update(id, req.body)
// allowlist: only fields this operation may change
const { label } = req.body
await db.invoices.update(id, { label })
The first version is a mass assignment vulnerability. A customer may change an invoice label but not isPaid or accountId — with the spread version, one crafted request flips both.
curl -i -X PATCH https://api.example.com/invoices/inv_9f3c2a \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label":"office chairs","isPaid":true,"accountId":"acc_admin"}'
# HTTP/1.1 400 Bad Request
# {"error":"fields not allowed: isPaid, accountId"}
Silently ignoring forbidden fields can preserve compatibility, while rejecting them makes client mistakes easier to detect. Choose one behavior and document it in the contract.
Allowlist output fields
The read side leaks the same way. Build response objects from approved fields and apply role-specific views where necessary. A customer view of an invoice never includes internalNote. A support view might, because support staff carry a different permission set over the same object.
ORMs make both leaks easy: toJSON() on a model serializes every column, and generated API clients happily send every field the schema exposes.
Verify the stored row, not the status code
The important evidence is the stored row and returned object, because a successful status alone can hide a partial unauthorized update:
SELECT label, is_paid, account_id
FROM invoices WHERE id = 'inv_9f3c2a';
-- label changed; is_paid and account_id untouched
Run this check after the malicious PATCH above. A 400 response paired with a flipped is_paid column means validation ran after the write — a bug the status code alone would never show you.
Send an update containing one allowed field and two protected fields. Prove the allowed value changed, the protected values did not, and the response never reveals internalNote.
Lesson completed