Fixing Access Control
Prevent unauthorized access to other users' data (IDOR, privilege escalation).
The problem
Broken access control is the #1 web vulnerability according to OWASP. It happens when a user can access or modify data that doesn't belong to them — usually by changing an ID in the URL or API request.
AI coding assistants are particularly bad at this. They generate CRUD endpoints that fetch data by ID without checking who's requesting it.
If changing /api/orders/123 to /api/orders/124 returns another user's order, your app has an IDOR vulnerability.
Types of access control failures
IDOR (Insecure Direct Object Reference)
The most common variant. User A can access User B's data by guessing or enumerating IDs:
// BAD — no ownership check
app.get('/api/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id);
res.json(order); // returns any user's order
});Privilege escalation
A regular user can perform admin actions:
// BAD — no role check
app.post('/api/users/:id/role', async (req, res) => {
await db.users.update(req.params.id, { role: req.body.role });
res.json({ success: true });
});Missing authentication
Endpoints that should require login but don't:
// BAD — no auth middleware
app.get('/api/admin/users', async (req, res) => {
const users = await db.users.findAll();
res.json(users);
});How to fix it
Always filter by the authenticated user
Every query that returns user-specific data must include an ownership check:
// GOOD — filter by authenticated user
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await db.orders.findOne({
where: {
id: req.params.id,
user_id: req.user.id, // ownership check
},
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});Use RLS for database-level enforcement
If you're using Supabase, RLS policies enforce access control at the database level — no matter how the data is queried:
CREATE POLICY "Users can only read own orders"
ON public.orders
FOR SELECT
USING (auth.uid() = user_id);See the RLS guide for complete setup instructions.
Check roles for admin actions
// GOOD — role check before admin action
app.post('/api/users/:id/role', auth, async (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
await db.users.update(req.params.id, { role: req.body.role });
res.json({ success: true });
});Use UUIDs instead of sequential IDs
Sequential IDs (1, 2, 3...) are trivial to enumerate. UUIDs make guessing much harder:
CREATE TABLE orders (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
-- ...
);UUIDs reduce the risk of enumeration but are not a substitute for proper access control. Always check ownership regardless of ID format.
Verify with Flowpatrol
Run a full scan to test access control:
Run a Flowpatrol scan on https://myapp.vercel.appThe scan authenticates as multiple test users and tries to access data across accounts.
Checklist
- Every API endpoint that returns user data filters by the authenticated user
- Admin endpoints check the user's role
- All endpoints require authentication (unless intentionally public)
- Database queries include ownership clauses (or RLS is enabled)
- IDs are UUIDs, not sequential integers
- List endpoints only return the current user's records
- File upload/download endpoints verify ownership