Direct naar inhoud

Onvoldoende autorisatie op objectniveau in API's

CWE-639CWE-285OWASP A01:2021Bijgewerkt 3 september 20265 min leestijd

Bij onvoldoende autorisatie op objectniveau controleert een API wel of de aanvrager is ingelogd, maar niet of het opgevraagde object van hem is. Door de identificatie in de URL te wijzigen, komt een geldige gebruiker zo bij de gegevens van anderen. Het is de meest voorkomende en meest misbruikte kwetsbaarheid in API's.

Van alle kwetsbaarheden in API’s is deze de meest voorkomende, en tegelijk de meest onopvallende: er gaat niets kapot, er verschijnt geen foutmelding en het verzoek is technisch volstrekt correct. Alleen krijgt de aanvrager gegevens te zien die niet van hem zijn. Hieronder leest u waarom deze fout zo hardnekkig is en hoe u haar structureel uitsluit.

Wat is autorisatie op objectniveau?

Bij elk verzoek naar een API moeten twee verschillende vragen worden beantwoord. De eerste is: wie bent u? Dat is authenticatie, en die wordt vrijwel altijd goed geregeld. De tweede is: mag u bij dít specifieke object? Dat is autorisatie op objectniveau, en die wordt regelmatig overgeslagen.

Onvoldoende autorisatie op objectniveau, internationaal bekend als broken object level authorization of BOLA, betekent dat de API het eerste wel controleert en het tweede niet. Iedereen met een geldig account kan dan bij de gegevens van elk ander account, simpelweg door de identificatie in het verzoek te wijzigen.

Denk aan een postkamer waar u zich netjes legitimeert bij binnenkomst, en vervolgens elk postvak mag openen dat u aanwijst. De legitimatie was echt, de medewerker heeft zijn werk gedaan, en toch staat u in andermans post. De controle die ontbreekt is niet “wie bent u” maar “is dit vakje van u”.

Hoe wordt gebrekkige autorisatie op objectniveau misbruikt?

Kwetsbaar:

// Er wordt gecontroleerd of iemand is ingelogd, en verder niets
app.get('/api/facturen/:id', vereistLogin, async (req, res) => {
  const factuur = await db.facturen.zoek(req.params.id);
  if (!factuur) return res.sendStatus(404);

  res.json(factuur);
});

De middleware vereistLogin doet precies wat de naam zegt: hij weigert verzoeken zonder geldige sessie. Wat hij niet doet, is nagaan of deze factuur bij deze gebruiker hoort. Een ingelogde klant hoeft dus alleen het nummer te wijzigen:

GET /api/facturen/10432 HTTP/1.1     ← eigen factuur
GET /api/facturen/10433 HTTP/1.1     ← factuur van een andere klant
HTTP/1.1 200 OK

Beide verzoeken slagen. En omdat het om een API gaat, is dit triviaal te automatiseren: een lus over alle nummers levert de complete facturenadministratie op. Ontbreekt er ook nog een snelheidsbeperking, dan is een volledig datalek een kwestie van minuten, uitgevoerd met een geldig account, zonder één foutmelding in uw logbestanden.

Veilig:

// De eigenaar is onderdeel van de opvraag, niet van een controle achteraf
app.get('/api/facturen/:id', vereistLogin, async (req, res) => {
  const factuur = await db.facturen.zoekVoorKlant(
    req.params.id,
    req.gebruiker.klantId,          // uit de sessie, nooit uit het verzoek
  );
  if (!factuur) return res.sendStatus(404);   // zelfde antwoord als 'bestaat niet'

  res.json(factuur);
});

Het verschil is klein in code en groot in gevolg. De eigenaar zit nu in de query zelf, en die eigenaar komt uit de sessie, niet uit een parameter die de aanvrager kan wijzigen. Bestaat de factuur wel maar hoort hij bij iemand anders, dan komt er niets terug.

Let op het gekozen antwoord: 404 en niet 403. Een 403 bevestigt dat het object bestaat, en dat is op zichzelf al informatie waarmee een aanvaller kan vaststellen welke identificaties in gebruik zijn.

Bij een groeiende applicatie is die controle per endpoint herhalen op den duur onhoudbaar. Robuuster is om het af te dwingen op een plek die niet vergeten kán worden:

// Elke query op deze repository krijgt de eigenaar er automatisch bij
const facturenVoor = (gebruiker) => ({
  zoek: (id) => db.facturen.eerste({ id, klant_id: gebruiker.klantId }),
  lijst: () => db.facturen.alle({ klant_id: gebruiker.klantId }),
});
Vertrouw nooit een identificatie van de eigenaar die in het verzoek zelf staat. Een parameter als klantId of een veld in de JSON-body wordt door de aanvrager bepaald en is dus geen bewijs van eigendom. De enige betrouwbare bron is de geverifieerde sessie of het geverifieerde token.

Wat is de impact van gebrekkige autorisatie op objectniveau?

De ernst is hoog tot kritiek, en dat is bij deze bevinding vrijwel altijd terecht. De reden is de combinatie van eenvoud en schaal.

De aanval vereist geen bijzondere kennis: een geldig account en het wijzigen van een getal volstaan. Er is geen injectie, geen omweg en geen exploit nodig. Daarnaast omdat API’s zijn ontworpen voor machinaal gebruik, laat de aanval zich moeiteloos automatiseren tot een volledige uitlezing van de dataset. Veel grote datalekken bij mobiele applicaties en klantportalen zijn langs precies deze weg ontstaan.

Betreft het endpoints die ook schrijven, dan blijft het niet bij lezen. Een PUT of DELETE zonder eigenaarscontrole betekent dat een gebruiker de gegevens van anderen kan wijzigen of verwijderen. Bij een endpoint dat rollen of rechten aanpast, ligt de weg open naar het verhogen van de eigen rechten.

Wat het detecteren bemoeilijkt: er is niets afwijkends te zien. Elk verzoek is geauthenticeerd, correct gevormd en beantwoord met een 200. Zonder controle op het patroon, één account dat honderden verschillende objecten opvraagt, valt het niet op.

Hoe spoor je gebrekkige autorisatie op objectniveau op?

De test vraagt twee accounts en systematiek. Een tester logt in als gebruiker A, doorloopt de applicatie en legt alle verzoeken vast waarin een identificatie voorkomt. Vervolgens herhaalt hij die verzoeken met het token van gebruiker B. Elk antwoord dat gegevens van A teruggeeft, is een bevinding.

Dat gebeurt niet alleen voor het opvragen. Juist de wijzigende en verwijderende endpoints worden vaak vergeten, terwijl de gevolgen daar groter zijn. Ook wordt gekeken naar identificaties die niet in het pad maar in de body, in een header of in een genest object staan: een bestellingId diep in een JSON-structuur wordt zelden gecontroleerd. Verder worden endpoints getest die meerdere objecten tegelijk verwerken, waar de controle soms alleen op het eerste element wordt uitgevoerd. Bovendien er wordt gelet op verschillen in het antwoord: een 403 waar anders een 404 komt, verraadt welke identificaties bestaan. AssistSec voert deze test uit met accounts in verschillende rollen en tussen verschillende klanten in een gedeelde omgeving, omdat juist de scheiding tussen klanten het vaakst onvolledig blijkt.

Hoe voorkom je gebrekkige autorisatie op objectniveau?

  • Controleer bij elk verzoek of het opgevraagde object toebehoort aan de geauthenticeerde gebruiker.
  • Neem de eigenaar op in de databasequery zelf, in plaats van achteraf te vergelijken.
  • Haal de identificatie van de eigenaar uitsluitend uit de sessie of het token, nooit uit het verzoek.
  • Dwing de controle centraal af, bijvoorbeeld in een repositorylaag, zodat een nieuw endpoint hem niet kan vergeten.
  • Antwoord met 404 in plaats van 403, zodat het bestaan van objecten niet wordt bevestigd.
  • Test elke methode afzonderlijk: opvragen, wijzigen, verwijderen en bulkbewerkingen.
  • Controleer ook identificaties die in de body of in geneste structuren staan.
  • Gebruik onvoorspelbare identificaties als aanvullende laag, nooit als vervanging van autorisatie.
  • Bewaak op accounts die ongewoon veel verschillende objecten opvragen.

Bronnen

Veelgestelde vragen

Helpt het om UUID's te gebruiken in plaats van oplopende nummers?

Het maakt raden lastiger, maar het lost niets op. Identificaties lekken via gedeelde links, exports, logbestanden en andere endpoints in dezelfde applicatie. Zodra een aanvaller er één kent, werkt de aanval onverminderd. Onvoorspelbare identificaties zijn een nuttige aanvulling, geen autorisatie.

Waarom komt dit zo vaak voor bij API's?

Omdat een API vaak per resource is opgezet en het autoriseren per endpoint moet worden herhaald. Eén nieuw endpoint waar de controle is vergeten, is genoeg. Bij een webinterface wordt het bovendien gemaskeerd doordat de gebruiker alleen zijn eigen links te zien krijgt; bij een API is dat schild er niet.

Wat is het verschil met BFLA?

Bij BOLA gaat het om de vraag of u bij dít object mag; bij BFLA om de vraag of u deze functie überhaupt mag uitvoeren. Het eerste is een gewone gebruiker die bij andermans record komt, het tweede een gewone gebruiker die een beheerdersfunctie aanroept. Beide moeten apart worden getest.

Hoe test ik dit systematisch?

Met twee accounts. Voer een handeling uit als gebruiker A, leg het verzoek vast, en herhaal het met het token van gebruiker B. Krijgt B de gegevens van A te zien, dan is de bevinding rond. Doe dat voor elk endpoint dat een identificatie accepteert, ook voor wijzigen en verwijderen.

Verwante artikelen

Druk op / om te zoeken · Esc