Permission Checks Are Where AI Code Lies Best

securityaiaccess-controlauthorization

You review a PR. It adds an endpoint that fetches an invoice by ID, checks the caller is authenticated and holds the invoices:read permission, and returns it. Compiles. Tests pass. Matches every other handler in the file.

It never checks whether the caller actually owns the invoice they’re asking for.

That’s an IDOR, insecure direct object reference, and it’s an easy pattern for AI to reproduce: the happy path looks identical whether the ownership check is there or not. The model’s pattern-matching your file’s shape, not reasoning about your access-control policy.

I would suggest picking up one habit (more like don’t let go of it): for any endpoint that reads, writes, or deletes a resource by ID, find the line that ties the caller’s identity to that specific resource. Not just to “is authenticated,” and not just to “has the right permission.” If that line doesn’t exist, the endpoint is broken, however clean everything else looks.

// requirePermission('invoices:read') already passed. Still wrong.
router.get('/invoices/:id', requireAuth, requirePermission('invoices:read'), (request) => {
const invoice = db.invoices.findById(request.params.id);
if (!invoice) return notFound();
return ok(invoice);
});
// The permission check was never the missing piece.
router.get('/invoices/:id', requireAuth, requirePermission('invoices:read'), (request) => {
const invoice = db.invoices.findById(request.params.id);
if (!invoice) return notFound();
if (invoice.ownerId !== request.user.id) return forbidden();
return ok(invoice);
});

Deny by default. Enforce it server-side, every time. Centralize the check instead of scattering it across handlers. And test it directly: two identities, one resource, assert one can’t touch the other’s data.

One missing line is the whole review.