JustAppSec

Authorisation and Access Control

RBAC, ABAC, and privilege escalation patterns in real applications.

0:00

Authentication answers "who are you?" Authorisation answers "what are you allowed to do?" Getting authorisation wrong means users can access, modify, or delete data that does not belong to them. This is consistently one of the most common and most impactful vulnerability classes.

Why authorisation bugs are so common

  • Authentication is usually centralised (one login system). Authorisation is distributed (every endpoint, every query, every feature has its own access rules).
  • Frameworks provide authentication middleware. They rarely provide authorisation middleware — you have to build it yourself.
  • Authorisation logic is business logic. It varies per feature, per role, per resource, and per context. It cannot be solved with a generic library.

IDOR — insecure direct object references

The simplest and most common authorisation bug. The application uses a user-controlled identifier (ID in the URL, request body, or query parameter) to fetch a resource, without checking whether the requesting user owns or is allowed to access that resource.

Example

GET /api/orders/4821

The server returns order 4821. But does the authenticated user own order 4821? If the server does not check, any authenticated user can access any order by changing the ID.

Fix

Always verify ownership or permission before returning data:

order = Order.objects.get(id=order_id)
if order.user_id != request.user.id:
    raise PermissionDenied()

Better yet, scope your queries:

# Only fetch orders belonging to the current user
order = Order.objects.get(id=order_id, user_id=request.user.id)

If the order does not exist for that user, it returns a 404 — which also avoids leaking that the order exists at all.

Access control models

Role-Based Access Control (RBAC)

Users are assigned roles. Roles have permissions. The check is: "does this user's role allow this action?"

admin → can create, read, update, delete
editor → can create, read, update
viewer → can read

RBAC is straightforward and works well for applications with a fixed set of roles and permissions. It breaks down when you need fine-grained, resource-level, or context-dependent access control.

Attribute-Based Access Control (ABAC)

Access decisions are based on attributes of the user, the resource, and the environment:

  • User attributes: role, department, clearance level
  • Resource attributes: owner, classification, sensitivity
  • Environment attributes: time of day, IP address, device type

Example policy: "A user can edit a document if they are in the same department as the document owner AND the document is not classified as restricted."

ABAC is more flexible than RBAC but harder to reason about and audit. It is best suited for complex enterprise environments.

Relationship-Based Access Control (ReBAC)

Access is determined by the relationship between the user and the resource. Think Google Docs: "this user is an editor of this document" rather than "this user has the editor role."

ReBAC handles hierarchical and graph-shaped permission models well. Tools like Zanzibar (Google), SpiceDB, and OpenFGA implement this model.

Choosing a model

ScenarioRecommended model
Simple app, few rolesRBAC
Multi-tenant SaaSRBAC per tenant, or ReBAC
Complex enterprise with contextual rulesABAC
Collaboration features (sharing, ownership)ReBAC

Most applications start with RBAC and evolve. That is fine — just plan for the evolution.

Common authorisation mistakes

Checking roles instead of permissions

# Fragile — tied to role names
if user.role == "admin":
    delete_user(target_id)

# Better — tied to permissions
if user.has_permission("users:delete"):
    delete_user(target_id)

Roles change. Permissions are more stable. Check permissions, not roles.

Client-side only enforcement

Hiding a button in the UI is not access control. If the API endpoint behind that button does not check permissions, the attacker will call it directly.

Every authorisation check must be enforced on the server. Client-side visibility is a UX convenience, not a security control.

Inconsistent enforcement

The /api/orders/:id GET endpoint checks ownership. The /api/orders/:id PUT endpoint does not. An attacker cannot view another user's order but can modify it. Consistency across all operations on a resource is essential.

Mass assignment enabling privilege escalation

If an API accepts arbitrary fields in a request body and applies them to a model without filtering:

PUT /api/users/me
{ "name": "Alice", "role": "admin" }

The user promotes themselves to admin. Always define an explicit allowlist of updatable fields.

Broken function-level authorisation

Admin endpoints like /api/admin/users often lack authorisation checks because "only admins know the URL." Obscurity is not security. Every endpoint must enforce its own access controls.

Implementing authorisation well

Centralise the policy

Define authorisation rules in one place, not scattered across individual route handlers. This makes the rules auditable, testable, and consistent.

# A central permission check
def check_permission(user, action, resource):
    if action == "orders:read" and resource.user_id == user.id:
        return True
    if action == "orders:read" and user.role == "admin":
        return True
    return False

Deny by default

Start with "deny everything" and explicitly grant access. If a new endpoint is added without an authorisation check, it should fail closed (deny access), not fail open.

Test authorisation explicitly

Write tests that verify:

  • User A cannot access User B's resources
  • A regular user cannot access admin endpoints
  • Each role can only perform its permitted actions
  • Downgrading a role immediately removes access
def test_user_cannot_access_other_users_order():
    order = create_order(user=user_a)
    response = client.get(f"/api/orders/{order.id}", auth=user_b)
    assert response.status_code == 404

Audit log access control decisions

Log when access is denied and when high-privilege actions are taken. If an attacker is probing for IDOR vulnerabilities, the denied-access logs will show a pattern of sequential ID access attempts.

Summary

Authorisation is the most frequently broken access control in web applications because it is distributed, business-specific, and easy to forget. Check ownership on every resource access. Use permissions instead of role names. Enforce everything on the server. Deny by default. Scope queries to the authenticated user. Test authorisation paths as rigorously as you test functionality.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].