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.

Being logged in doesn’t mean you may see every record. Authentication answers who you are. Authorization answers what you may touch. Changing /notes/123 to /notes/124 in the address bar must not cross from your data into someone else’s. When it does, that’s broken object-level authorization, and it sits near the top of every list of real-world web flaws.

The route that authenticates and still leaks

This handler requires a login. It still gives away 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 logs in and opens /notes/42, her note. She edits the URL to /notes/43. The route sees a valid session, runs the query, and returns Bob’s note, because the query only asks for the id. No error, no alarm. Just a 200 with someone else’s data.

You can reproduce this in a minute with two test accounts and curl:

curl -s -b 'session=alice' https://app.flaviocopes.com/notes/43

If that prints Bob’s note, you have the bug.

Bind the query to the trusted owner

The fix is to scope every query by the identity that comes from the session, never from the request:

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()

Run the curl again. The database finds no row where id = 43 and owner_id = alice, and the route answers 404. Notice it’s a 404, not a 403. “Not found” tells Alice nothing about whether note 43 exists.

Apply the same rule everywhere: reads, updates, deletes, exports, search, and bulk operations. Bulk endpoints are where I see this fail most. A POST /notes/bulk-delete that checks ownership on the first ID and then deletes the whole list has passed a lot of code reviews.

Things that feel like authorization and aren’t

Hiding the “Edit” button for other people’s notes is UI, not permission. Anyone can send the request without the button. Using UUIDs instead of 42 makes guessing harder, but IDs leak through shared links, logs, and other API responses. Neither replaces the owner_id in the query.

My habit is to make it hard to forget. A helper like notesFor(req.user.id) that always adds the owner clause means a new route can’t skip it by accident.

Try this on your own project: create two accounts and one note per account. As Alice, try read, update, delete, export, and search against Bob’s note, and save each response. Bob’s data must never appear in a response or change in the database.

Lesson completed