API-antwoorden bevatten te veel gegevens
CWE-213CWE-200OWASP A01:2021Bijgewerkt 3 september 20265 min leestijd
Veel API's geven het volledige databaserecord terug en laten het aan de frontend over om te bepalen wat er wordt getoond. Wat op het scherm onzichtbaar blijft, staat wel gewoon in het antwoord: wachtwoordhashes, interne notities, gegevens van andere gebruikers. Filteren hoort aan serverzijde te gebeuren.
Bij het bouwen van een API is het verleidelijk om het volledige record terug te geven en de frontend te laten kiezen wat ervan zichtbaar wordt. Het scheelt werk, het maakt de API flexibel, en op het scherm ziet niemand het verschil. In het netwerkverkeer wel, en daar kijkt uiteindelijk iedereen die dat wil. Hieronder leest u wat er zoal meelift en hoe u dat structureel afsluit.
Wat is te veel gegevens teruggeven?
Van te veel gegevens in API-antwoorden spreken we wanneer een endpoint meer velden teruggeeft dan de aanvrager nodig heeft of mag zien, in de veronderstelling dat de client wel filtert. De term die internationaal wordt gebruikt is excessive data exposure, tegenwoordig ondergebracht bij autorisatie op eigenschapsniveau.
De denkfout zit in de aanname over wie de API gebruikt. Tijdens de ontwikkeling is dat uw eigen frontend, en die toont netjes alleen de bedoelde velden. Alleen de API staat open voor iedereen met een geldig account, en het antwoord is zichtbaar in het ontwikkelaarsvenster van elke browser. Alles wat u meestuurt, is daarmee uitgeleverd.
Vergelijk het met een dossier waarvan u de gevoelige pagina’s afdekt met een blaadje op het moment dat u het aan iemand laat zien. Zolang de ander netjes blijft kijken naar wat u aanwijst, gaat het goed. Maar u hebt hem het hele dossier in handen gegeven, en het blaadje is niet vastgeplakt.
Hoe ziet zo’n API-antwoord eruit?
Kwetsbaar:
// Het volledige record gaat naar buiten
app.get('/api/gebruikers/:id', vereistLogin, async (req, res) => {
const gebruiker = await db.gebruikers.zoek(req.params.id);
res.json(gebruiker);
});
De interface toont een profielkaartje met een naam en een foto. Het antwoord bevat aanzienlijk meer:
{
"id": 4471,
"naam": "J. Dekker",
"email": "j.dekker@bedrijf.nl",
"wachtwoord_hash": "$2b$12$c8y3Nq0Wl0mVQe1n6JbEZe...",
"telefoon": "+31 6 12345678",
"bsn": "123456782",
"salaris_schaal": 11,
"interne_notitie": "Functioneringstraject loopt tot Q3",
"mfa_secret": "JBSWY3DPEHPK3PXP",
"rol": "medewerker"
}
Hier gaat het meerdere keren tegelijk mis. De wachtwoordhash is materiaal voor een offline kraakpoging. Het mfa_secret maakt het mogelijk zelf geldige tweefactorcodes te genereren, waarmee die hele maatregel wordt omzeild. De interne notitie en de salarisschaal zijn persoonsgegevens die niet eens voor de betrokkene zelf bedoeld waren. Bovendien omdat het één endpoint is dat op identificatie werkt, is het geheel eenvoudig systematisch af te lopen.
Veilig:
// Expliciet vastleggen wat naar buiten mag, per rol
function publiekProfiel(gebruiker) {
return {
id: gebruiker.id,
naam: gebruiker.naam,
afdeling: gebruiker.afdeling,
};
}
function eigenProfiel(gebruiker) {
return {
...publiekProfiel(gebruiker),
email: gebruiker.email,
telefoon: gebruiker.telefoon,
};
}
app.get('/api/gebruikers/:id', vereistLogin, async (req, res) => {
const gebruiker = await db.gebruikers.zoek(req.params.id);
if (!gebruiker) return res.sendStatus(404);
const eigen = gebruiker.id === req.gebruiker.id;
res.json(eigen ? eigenProfiel(gebruiker) : publiekProfiel(gebruiker));
});
Het bepalende verschil is de richting van de keuze. Er wordt vastgelegd wat er wél naar buiten mag, in plaats van wat er moet worden weggelaten. Dat is belangrijker dan het lijkt: bij een lijst met uitzonderingen belandt elk nieuw databaseveld automatisch in het antwoord, en niemand denkt daaraan bij het toevoegen van een kolom.
Vraag daarnaast alleen op wat u nodig hebt. Een query die SELECT * vermijdt, kan de gevoelige velden niet per ongeluk meesturen omdat ze er simpelweg niet zijn.
Wat is de impact van te veel gegevens in API-antwoorden?
De ernst loopt van middelzwaar tot hoog en wordt bepaald door de gelekte velden. Gaat het om een intern volgnummer, dan is het effect gering. Gaat het om persoonsgegevens, wachtwoordhashes of geheimen voor tweefactorauthenticatie, dan is het ernstig.
Het lastige is dat het lek volledig onzichtbaar blijft in de gebruikelijke controles. Er is geen foutmelding, geen afwijkend gedrag en geen ongewoon patroon in de logbestanden; het endpoint doet exact wat het is opgedragen. De bevinding komt aan het licht bij een test, of doordat de gegevens ergens anders opduiken.
In combinatie met andere zwakke plekken wordt de impact snel groter. Ontbreekt ook de autorisatie op objectniveau, dan kan een aanvaller de volledige gebruikersadministratie ophalen inclusief alle gevoelige velden. Ontbreekt een snelheidsbeperking, dan gebeurt dat in minuten. Wat afzonderlijk drie middelzware bevindingen zijn, is samen een datalek dat onder de meldplicht valt.
Hoe spoor je te veel gegevens in API-antwoorden op?
Een tester kijkt niet naar het scherm maar naar het netwerkverkeer. Elk API-antwoord wordt vergeleken met wat de interface ervan toont; het verschil is de bevinding. Dat levert vaak direct resultaat op, omdat de discrepantie zelden klein is.
Daarnaast wordt gelet op de velden die niet in de interface voorkomen maar wel een naam hebben die iets verraadt: alles met hash, secret, token, intern of notitie. Ook worden de lijstendpoints bekeken, waar hetzelfde object vaak in een andere vorm terugkomt dan bij het opvragen van één item, en waar de filtering dan net niet is toegepast. Verder wordt gekeken naar antwoorden op fouten en naar geneste objecten, en bij GraphQL naar velden die in het schema staan maar door de eigen frontend nooit worden opgevraagd. AssistSec beoordeelt daarbij of de velden per rol verschillen, omdat een beheerdersweergave en een gebruikersweergave vaak door hetzelfde endpoint worden bediend zonder onderscheid in de uitvoer.
Hoe voorkom je te veel gegevens in API-antwoorden?
- Bepaal expliciet welke velden een endpoint teruggeeft in plaats van welke u weglaat.
- Definieer per rol een eigen uitvoermodel en serialiseer daar doorheen.
- Vraag in uw queries alleen de kolommen op die u nodig hebt; vermijd
SELECT *. - Geef nooit wachtwoordhashes, tweefactorgeheimen, herstel-tokens of API-sleutels terug.
- Behandel geneste objecten met dezelfde zorg als het hoofdobject.
- Laat filteren nooit aan de frontend over; dat is presentatie, geen beveiliging.
- Autoriseer bij GraphQL per veld, niet alleen per query.
- Controleer bij elk nieuw databaseveld of het onbedoeld in bestaande antwoorden terechtkomt.
- Neem een controle op de uitvoer op in uw geautomatiseerde tests, zodat een nieuw veld opvalt.
Bronnen
Veelgestelde vragen
De gebruiker ziet die velden toch niet in de interface?
In de interface niet, in het antwoord wel. Iedereen kan de netwerkverzoeken van zijn eigen browser bekijken of de API rechtstreeks aanroepen. Wat de frontend verbergt is uitsluitend een presentatiekeuze; het is geen beveiliging en het is met twee muisklikken te omzeilen.
Hoe voorkom ik dit structureel?
Door expliciet te bepalen welke velden naar buiten gaan in plaats van welke u weglaat. Definieer per endpoint en per rol een uitvoermodel, en serialiseer daar doorheen. Een nieuw veld in de database komt dan niet vanzelf in het antwoord terecht, wat precies het foutmechanisme is dat u wilt uitsluiten.
Geldt dit ook voor GraphQL?
Ja, met een eigen accent. Bij GraphQL bepaalt de client welke velden hij opvraagt, dus de autorisatie moet per veld gebeuren. Een veld dat in het schema staat maar niet is beveiligd, is opvraagbaar, ook al vraagt uw eigen frontend er nooit om.
Is dit hetzelfde als een datalek?
Het is er de oorzaak van, zodra iemand het opmerkt. Vaak wordt de bevinding pas gedaan bij een test of nadat gegevens elders opduiken. Het bijzondere is dat er niets is ingebroken: de gegevens werden gewoon uitgeleverd aan iedereen die erom vroeg.
Verwante artikelen
- KwetsbaarhedenCWE-285A01:2021Onvoldoende autorisatie op functieniveauEen beheerdersfunctie die alleen uit het menu is weggelaten, blijft bereikbaar via een direct verzoek. Lees hoe u autorisatie per functie afdwingt.
- KwetsbaarhedenCWE-639A01:2021Onvoldoende autorisatie op objectniveau in API'sEen API die alleen controleert óf u bent ingelogd en niet óf dit record van u is, geeft andermans gegevens prijs. Lees hoe u dat afdwingt.
- KwetsbaarhedenCWE-200A01:2021Information disclosureInformation disclosure uitgelegd: hoe stack traces, .git-mappen, source maps en te ruime API-responses gegevens lekken, en hoe u dat voorkomt.
- KwetsbaarhedenCWE-915A01:2021Mass assignmentMass assignment uitgelegd: hoe een ongefilterde request body velden als isAdmin of balance schrijfbaar maakt en hoe een allowlist dat voorkomt.
- KwetsbaarhedenCWE-770A04:2021Geen snelheidsbeperking op de APIZonder snelheidsbeperking kan een API onbeperkt worden bevraagd. Lees hoe dat leidt tot datadiefstal, misbruik van kosten en uitval.