Access and configuration
Enforce object authorization
Use trusted identity and server-owned relationships for every read and write instead of treating object IDs or hidden controls as permission.
A signed-in user is not authorized for every record. Authentication proves who someone is; authorization decides what they may touch. Changing /notes/123 to /notes/124 must not cross an ownership boundary. This gap is called broken object-level authorization, and it is one of the most common web flaws.
The route below authenticates correctly and still leaks data.
// Vulnerable: checks the session but not ownership
app.get('/notes/:id', requireLogin, async (req, res) => {
const note = await db.query('SELECT * FROM notes WHERE id = ?', [req.params.id])
res.json(note)
})
Alice requests /notes/42, then changes the ID to Bob’s note. The route sees a valid session and returns the record because the query checks only id.
Bind the query to the trusted owner
The fix scopes every query by the identity from the session, never from the request. The database returns nothing when the owner does not match.
const note = await db.query(
'SELECT * FROM notes WHERE id = ? AND owner_id = ?',
[req.params.id, req.user.id], // owner comes from the session, not the URL
)
if (!note) return res.status(404).end()
Apply the same rule to reads, updates, deletes, exports, search, and bulk operations. A bulk endpoint that checks only the first item is a common miss.
Hiding the note ID or using a UUID makes guessing harder but does not create permission. The query still needs the trusted owner or tenant relationship.
Use two valid accounts to test read, update, delete, export, and search for the same note. Save each response and verify Bob’s stored data never appears or changes when Alice acts.
Lesson completed