Broken Access Control
Bron: PortSwigger Web Security Academy
Auteur: Johan Beysen
1. Wat is Broken Access Control?
Broken Access Control treedt op wanneer een applicatie gebruikers toelaat acties uit te voeren of data te bekijken waarvoor ze geen rechten hebben. Het is consistent de meest voorkomende kwetsbaarheid in de OWASP Top 10.
Twee hoofdtypes:
| Type | Omschrijving |
|---|---|
| Verticale BAC | Lager-geprivilegieerde gebruiker krijgt toegang tot admin/hogere functies |
| Horizontale BAC | Gebruiker krijgt toegang tot data van een andere gebruiker met gelijke rechten |
Impact:
- Ongeautoriseerde toegang tot admin functionaliteit
- Inzage in gevoelige data van andere gebruikers
- Account takeover
- Data manipulatie of verwijdering
2. Detectie
| Test | Wat je zoekt |
|---|---|
robots.txt, sitemap.xml |
Paden van admin of gevoelige endpoints |
| JS broncode (View Source) | Hardcoded paden of role-checks |
| Cookies en headers | Client-stuurbare role/privilege-waarden |
| ID-parameters swappen | Horizontale toegang tot andere accounts |
| HTTP methode wisselen | Check enkel op POST maar niet op GET |
| Stap 2 van multi-step direct sturen | Ontbrekende check op bevestigingsstappen |
Burp Suite
Gebruik Burp Repeater om raw responses te bekijken, ook bij redirects. Burp Spider pikt hardcoded paden in JS automatisch op.
3. Admin Functionaliteit Onbeveiligd
3.1 Informatielekken via robots.txt
robots.txt instrueert zoekmachines welke paden ze moeten overslaan. Het is publiek toegankelijk.
Probeer elk Disallow-pad direct. Geen authenticatie? Dan is het raak.
Ironisch
Door een pad te "verbergen" voor zoekmachines maak je het zichtbaar voor elke aanvaller. robots.txt is geen beveiligingsmaatregel.
Kijk ook naar /sitemap.xml voor vergelijkbare lekken.
Fix:
- Authenticatie en autorisatie afdwingen op de admin route zelf
- Admin panel achter VPN of IP-whitelist
3.2 Paden in JS Broncode
Het admin pad staat hardcoded in client-side JavaScript — zichtbaar voor iedereen, ongeacht de UI-staat.
var isAdmin = false;
if (isAdmin) {
var adminPanelTag = document.createElement('a');
adminPanelTag.setAttribute('href', '/admin-7hf93k'); // ← pad gelekt
}
Zoek via View Source (Ctrl+U) of DevTools → Sources:
Fix:
- Nooit gevoelige paden of logica in client-side JS zetten
- JS minification/obfuscation is geen beveiliging
4. Client-Side Enforcement Bypasses
4.1 Role via Cookie Parameter
Na inloggen zet de server een cookie die de rol bepaalt. De applicatie vertrouwt blind op die waarde.
Set-Cookie: Admin=false ← door server gestuurd
→ Verander naar: Admin=true
→ Toegang tot admin panel verkregen
Zoek in DevTools → Application → Cookies naar:
Kernprobleem
Access control mag nooit gebaseerd zijn op client-stuurbare waarden. De client kan alles sturen wat hij wil — cookies, headers, body parameters.
Fix:
- Rol/rechten uitsluitend server-side opslaan, in de sessie
- Session ID in cookie, rechten ophalen uit database op basis van die sessie
4.2 Mass Assignment via JSON Body
De server accepteert extra JSON-velden in een request die hij niet verwacht:
// Normale request:
{"email": "user@example.com"}
// Aanvalsrequest:
{"email": "user@example.com", "roleid": 2}
// Response:
{"username": "user", "email": "...", "roleid": 2} ← gelukt
De server mapt de JSON input direct naar een object zonder whitelist van toegestane velden. Dit heet mass assignment of auto-binding.
Herkennen: API responses bevatten meer velden dan je verstuurt (roleid, isAdmin, verified...). Die extra velden in de response → probeer ze ook te sturen in requests.
Fix:
- Explicit whitelist van toegestane velden bij model binding
- Laravel:
$fillable| Rails: strong parameters | Spring:@JsonIgnore
5. Header-Based Bypasses
5.1 X-Original-URL
De front-end proxy blokkeert /admin op URL. De back-end ondersteunt de X-Original-URL header.
Front-end ziet / → geen blokkering. Back-end ziet de header → serveert /admin.
Query parameters bij acties meegeven via de normale URL:
Testmethode:
- Probeer
/admindirect → geblokkeerd GET /+X-Original-URL: /invalid→ "not found" van back-end? Header wordt verwerktX-Original-URL: /admin→ toegang
Vergelijkbare headers
X-Rewrite-URL, X-Forwarded-URL
Fix:
X-Original-URLstrippen uit inkomende externe requests op de proxy- Back-end access control implementeren, niet enkel op de proxy laag
5.2 Referer-Based Access Control
De server controleert of de Referer header verwijst naar een admin pagina:
GET /admin-roles?username=wiener&action=upgrade HTTP/1.1
Referer: https://vulnerable-site.com/admin ← zelf toegevoegd
Cookie: session=<non-admin-session>
→ 200 OK, actie uitgevoerd
De Referer header is volledig client-stuurbaar.
| Header | Ogenschijnlijk doel | Realiteit |
|---|---|---|
Referer |
Vorige pagina aanduiden | Vrij instelbaar door client |
X-Original-URL |
URL override | Vrij instelbaar door client |
X-Forwarded-For |
Origineel IP bij proxy | Vrij instelbaar door client |
Cookie (role=admin) |
Sessie-info | Vrij instelbaar door client |
Fix:
- Nooit access control baseren op
Referer,Originof andere client-stuurbare headers
6. HTTP Method Bypass
De access control check zit enkel op POST-requests:
Burp tip
"Change request method" in Burp Repeater verplaatst POST body-parameters automatisch naar de query string bij conversie naar GET.
Fix:
- Access control binden aan de route, niet aan de methode
- Onbekende methodes →
405 Method Not Allowed
7. Multi-Step Process Bypass
Access control enkel op stap 1, niet op de bevestigingsstap:
Stap 1: POST /admin/promote → 403 (check aanwezig)
Stap 2: POST /admin/promote/confirm → 200 OK (geen check)
Kernfout
Developers gaan ervan uit dat stap 2 enkel bereikbaar is via stap 1. Maar HTTP heeft geen "state" — elke request is onafhankelijk.
Testmethode:
- Log in als admin, voer de volledige flow uit, intercept alle requests in Burp
- Log in als gewone user, kopieer je session cookie naar de stap-2-request
- Stuur stap 2 direct, zonder stap 1 te doen
Fix:
- Access control op elke stap van een multi-step flow
- Server-side state bijhouden: controleer of stap 1 succesvol doorlopen is
8. IDOR (Insecure Direct Object Reference)
De applicatie gebruikt een gebruiker-gecontroleerde waarde om direct een object op te halen, zonder te controleren of die gebruiker daar toegang toe heeft.
8.1 Voorspelbaar ID
Testen:
- Maak twee accounts aan
- Log in als account A, pas
id-parameter aan naar ID van account B - Burp Intruder: brute force numerieke IDs
Fix:
- Server-side controleren of de ingelogde gebruiker eigenaar is van het gevraagde object
- Indirect references (mapping table) in plaats van directe DB-IDs
8.2 GUID / Onvoorspelbaar ID
De applicatie gebruikt GUIDs maar die lekken via andere pagina's:
Blog post auteur-URL: /blogs?userId=fe89a329-d98f-4a26-b... ← GUID gelekt
Account URL: /my-account?id=fe89a329-d98f-4a26-b... ← zelfde GUID
Kernprincipe
Een onvoorspelbaar ID is geen access control. Security through obscurity: als het ID ergens anders lekt, is de "beveiliging" volledig nutteloos.
Zoek of user-IDs ergens in de applicatie voorkomen: blog auteurs, forum posts, reacties, e-mail headers.
Fix:
- GUID ≠ access control: autorisatiecheck blijft verplicht
- Nooit user-specifieke IDs lekken in publiek toegankelijke content
8.3 Data Leakage in Redirect
De server stuurt een 302 redirect als je een ID opvraagt dat niet van jou is. Maar de response body bevat de gevoelige data al — vóór de redirect uitgevoerd wordt.
HTTP/1.1 302 Found
Location: /home
<body>
API key: abc123xyz... ← browser negeert dit, Burp wel
</body>
| Tool | Gedrag bij 302 redirect |
|---|---|
| Browser | Negeert body, volgt redirect |
| Burp Repeater | Toont volledige raw response inclusief body |
Fix:
- Response body leegmaken vóór de redirect verstuurd wordt
- Data pas ophalen na succesvolle autorisatiecheck
8.4 Bestanden met Voorspelbare Naam
Bestanden opgeslagen met een incrementeel of voorspelbaar nummer:
GET /download-transcript/3.txt ← jouw bestand
GET /download-transcript/1.txt ← bestand van iemand anders
Fix:
- Bestanden opslaan met niet-voorspelbare namen (UUID)
- Server-side controleren of de gebruiker eigenaar is van het bestand
8.5 Password Disclosure via IDOR
De profielpagina geeft het wachtwoord terug in de response:
GET /my-account?id=wiener ← eigen wachtwoord zichtbaar
GET /my-account?id=administrator ← admin wachtwoord zichtbaar
Impact
Naast data-inzage leidt dit direct tot account takeover. Impact: maximaal.
Fix:
- Wachtwoorden nooit in responses sturen, ook niet masked
- Autorisatiecheck: controleer of de sessiegebruiker = eigenaar van het gevraagde account
- Wachtwoorden alleen gehasht opslaan
9. Patronen & Gouden Regels
De Drie Rode Draden
1. Beveiliging gebaseerd op client-stuurbare waarden
Cookies, headers, query parameters, JSON body — de client controleert dit allemaal. Vertrouw het nooit voor access control beslissingen.
2. Access control in één laag, omzeilbaar via een andere vector
Front-end blokkeert? Probeer de back-end direct. Check enkel op POST? Probeer GET. Enkel stap 1 beveiligd? Stuur stap 2 rechtstreeks.
3. Gevoelige data lekt via onverwachte kanalen
Redirect bodies, JS broncode, robots.txt, blog auteur-URLs, API responses — data lekt overal als developers er niet actief op letten.
Samenvatting per Techniek
| Techniek | Kernles |
|---|---|
| robots.txt leak | robots.txt is geen beveiliging |
| JS broncode leak | Client-side JS verbergt niets |
| Cookie role param | Nooit rechten in client-stuurbare cookies |
| Mass assignment | Whitelist je model binding |
| X-Original-URL | Headers zijn client-stuurbaar |
| Method bypass | Check de route, niet de methode |
| Multi-step bypass | Elke stap afzonderlijk beveiligen |
| Referer check | Referer is client-stuurbaar |
| IDOR simpel ID | Altijd autorisatiecheck op object |
| IDOR GUID | GUID ≠ access control |
| IDOR redirect leak | Body leegmaken voor redirect |
| IDOR bestanden | Bestanden niet direct ophaalbaar met voorspelbare naam |
| Password via IDOR | Wachtwoorden niet in responses |
Gouden regels
- Vertrouw nooit client-gestuurde input voor autorisatiebeslissingen
- Implementeer access control op de back-end, op elke route, elke methode, elke stap
- Controleer altijd: is de ingelogde gebruiker eigenaar van het gevraagde object?
- Gevoelige data pas ophalen na succesvolle autorisatiecheck