Access Control Hardening
Server-side ownership checks, deny-by-default, centralised gate functions, and the audit query that finds every missing check before an attacker does.
Server-side ownership checks, deny-by-default, centralised gate functions, and the audit query that finds every missing check before an attacker does.
Hardening access control means moving from ad-hoc, per-endpoint checks to a systematic architecture where every request passes through a consistent authorisation layer. It combines centralised gate functions, deny-by-default policies, server-side ownership verification, and comprehensive audit logging.
A single missing access check is all an attacker needs. When authorisation logic is scattered across individual route handlers, it is inevitable that some endpoints will be missed — especially as the codebase grows and new features are added under deadline pressure. Hardened access control replaces this fragile pattern with a structured layer that every request must pass through.
The four pillars of access hardening are: centralised gate functions that enforce role and ownership checks in one place; a deny-by-default policy that returns 403 for any request that does not have an explicit allow rule; server-side ownership checks that always derive identity from the session; and audit logging that records every access decision for later review.
The sandbox below simulates an audit dashboard that logs every access control decision. Toggle between a vulnerable configuration (missing ownership checks) and a hardened configuration (centralised gate + audit logging). Watch how the same attacker requests produce very different outcomes — and how the audit trail makes the difference visible.
In 2018, Facebook disclosed a vulnerability in the “View As” feature that allowed attackers to view any user's profile as if they were that user. The feature was designed to let users preview how their profile looks to a specific friend, but a rarely-used code path — triggered by a specific combination of video upload logic and birthday wishes — skipped the ownership check entirely.
The bug affected 50 million accounts and required Facebook to invalidate access tokens for all of them. The root cause was not that Facebook lacked access controls, but that the check was implemented in individual handlers rather than a centralised gate. One handler out of dozens was missing the check, and the attacker found it. This is the textbook argument for centralised authorisation: humans make mistakes, but a single gate function is much harder to forget.
A hardened access control system uses a gate function that every protected route must call. The gate checks authentication, role permissions, and resource ownership. Any request that does not match an explicit allow rule is denied by default — not accidentally permitted.
// VULNERABLE - ad-hoc checks scattered across handlers
app.get('/api/tickets/:id', async (req, res) => {
// developer forgot ownership check
const ticket = await db.query(
'SELECT * FROM tickets WHERE id = $1',
[req.params.id],
);
res.json(ticket.rows[0]);
});
// HARDENED - centralised gate + deny-by-default
type Permission = { resource: string; action: string };
async function authorize(req: Request, resource: string, ownerId?: string) {
const user = req.session.user;
if (!user) throw new AuthError('unauthenticated', 401);
// Deny by default
const allowed = user.permissions.some(
(p: Permission) => p.resource === resource,
);
if (!allowed) throw new AuthError('forbidden', 403);
// Ownership check
if (ownerId && user.id !== ownerId) {
throw new AuthError('not owner', 403);
}
}
app.get('/api/tickets/:id', async (req, res) => {
await authorize(req, 'tickets');
const ticket = await db.query(
'SELECT * FROM tickets WHERE id = $1 AND assigned_agent_id = $2',
[req.params.id, req.session.userId],
);
if (!ticket.rows[0]) return res.status(403).end();
// Audit log
console.log(`[AUDIT] user=${req.session.userId} ticket=${req.params.id} ALLOWED`);
res.json(ticket.rows[0]);
});Complement the gate with structured audit logging. Log every authorisation decision — both allowed and denied — with the user ID, resource ID, action, and timestamp. In a security incident, the audit log is the fastest way to determine what happened, what data was exposed, and which users were affected.
1.What is the primary advantage of a centralised authorisation gate?
2.What does "deny by default" mean in access control?
3.A developer adds an audit log that only records successful requests. What is the problem?