GraphQL-introspectie en onbeschermde velden
CWE-200CWE-285OWASP A05:2021Bijgewerkt 3 september 20265 min leestijd
Met introspectie kan iedereen het volledige GraphQL-schema opvragen: elk type, elk veld en elke mutatie, ook wat uw eigen frontend nooit gebruikt. Omdat de client bij GraphQL zelf bepaalt welke velden hij opvraagt, moet de autorisatie per veld gebeuren. Gebeurt dat niet, dan is alles wat in het schema staat ook opvraagbaar.
GraphQL verplaatst een deel van de regie naar de client: die bepaalt zelf welke velden hij opvraagt. Dat is de kracht van het model en tegelijk de reden dat de beveiliging er anders werkt dan bij een klassieke API. Hieronder leest u waarom een controle per endpoint hier niet volstaat en wat introspectie daarbij prijsgeeft.
Wat is introspectie?
Introspectie is een ingebouwde eigenschap van GraphQL waarmee een client het volledige schema kan opvragen: alle typen, alle velden met hun datatypes, alle queries en alle mutaties. De functie bestaat om gereedschap mogelijk te maken (een editor die aanvult terwijl u typt, documentatie die zichzelf genereert) en is tijdens ontwikkeling bijzonder nuttig.
In productie is het een volledige inhoudsopgave van uw gegevensmodel, opvraagbaar door iedereen die het endpoint kan bereiken. Daarbij komt het tweede en belangrijkere punt: omdat de client zelf bepaalt welke velden hij opvraagt, is het schema tegelijk een lijst van wat er beschikbaar is. Ontbreekt een controle per veld, dan is alles wat in het schema staat ook daadwerkelijk op te halen.
Het verschil met een klassieke API zit precies daar. Bij REST bepaalt u per endpoint wat er teruggaat; het aantal mogelijke antwoorden is eindig en overzichtelijk. Bij GraphQL bepaalt de client de vorm van zijn vraag, en is uw schema de grens. Wat u in het schema zet, hoort u ook te beveiligen.
Hoe misbruikt een aanvaller introspectie?
Kwetsbaar:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // ook in productie
});
# Iedereen kan het volledige schema opvragen
query { __schema { types { name fields { name type { name } } } } }
Uit het antwoord blijkt welke typen er zijn, en daarbij velden die de frontend nooit gebruikt. Een aanvaller stelt vervolgens gewoon zijn eigen vraag:
query {
gebruikers {
id
naam
email
wachtwoordHash # staat in het schema, dus opvraagbaar
mfaSecret
interneNotitie
}
}
De officiële frontend vraagt alleen om id en naam. Dat de overige velden bestaan, is voldoende: er is geen controle die zegt dat wachtwoordHash niet voor deze gebruiker bedoeld is. Een enkele query levert daarmee gegevens op die via een klassieke API nooit een endpoint zouden hebben gekregen.
Veilig:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
validationRules: [depthLimit(7), complexityLimit(1000)],
formatError: (fout) => ({ message: 'Verzoek kon niet worden verwerkt' }),
});
// Autorisatie op het veld zelf, niet op de query
const resolvers = {
Gebruiker: {
email: (ouder, args, ctx) => {
if (ctx.gebruiker.id !== ouder.id && ctx.gebruiker.rol !== 'beheerder') {
return null;
}
return ouder.email;
},
// Velden die nooit naar buiten mogen, horen niet in het schema
},
Query: {
gebruikers: async (ouder, args, ctx) => {
if (!ctx.gebruiker) throw new GraphQLError('Niet geauthenticeerd');
return gebruikers.zichtbaarVoor(ctx.gebruiker);
},
},
};
Er gebeuren hier vier dingen. Introspectie staat alleen aan buiten productie. De diepte en complexiteit van queries zijn begrensd, wat overbelasting via diep geneste vragen voorkomt. Foutmeldingen geven geen veldsuggesties meer. En het belangrijkste: de autorisatie zit op het veld, waar hij hoort. Velden die onder geen enkele omstandigheid naar buiten mogen (wachtwoordhashes, tweefactorgeheimen) staan helemaal niet in het schema.
Wat is de impact van GraphQL-introspectie?
De ernst loopt van middelzwaar tot hoog en hangt af van wat er zonder veldcontrole bereikbaar is. Bij een schema waarin alleen publieke gegevens staan, blijft het bij informatie over uw gegevensmodel. Staan er gevoelige velden in zonder eigen autorisatie, dan is het een datalek dat met één query wordt uitgevoerd.
Wat het risico vergroot is de combinatie met andere eigenschappen van GraphQL. Er is doorgaans één endpoint, waardoor beperkingen per endpoint weinig betekenen. Eén verzoek kan gegevens uit veel typen tegelijk ophalen, waardoor een aanvaller in weinig verzoeken veel binnenhaalt. Daarnaast geneste relaties maken het mogelijk om via een omweg bij gegevens te komen die op het hoofdniveau wel beschermd zijn, bijvoorbeeld door de gebruiker van een bericht op te vragen in plaats van de gebruiker rechtstreeks.
Daar komt de belastingkant bij. Een diep geneste query kan de server dwingen tot een enorme hoeveelheid werk, wat zonder diepte- en complexiteitsgrenzen tot uitval kan leiden.
Hoe spoor je GraphQL-introspectie op?
Een tester probeert eerst of introspectie is ingeschakeld met een standaard schemaquery. Lukt dat, dan is het volledige schema beschikbaar en verschuift de aandacht meteen naar de velden die de frontend niet gebruikt.
Staat introspectie uit, dan wordt het schema alsnog gereconstrueerd: uit de JavaScript-bundel, uit de queries die de applicatie zelf verstuurt, en door veldnamen te proberen en op de foutmeldingen te letten, implementaties die suggesties doen bij een typefout, verraden daarmee welke velden wél bestaan. Vervolgens wordt per gevonden veld getoetst of er autorisatie op zit, met een account met minimale rechten. Ook wordt gekeken naar geneste paden die om een controle heen leiden, naar de begrenzing van diepte en complexiteit, en naar de vraag of mutaties dezelfde controles kennen als queries. AssistSec test GraphQL-endpoints daarom apart van de rest van de API, omdat standaardhulpmiddelen die op REST zijn gericht hier weinig zichtbaar maken.
Hoe voorkom je GraphQL-introspectie?
- Schakel introspectie uit in productie en houd hem alleen aan in ontwikkelomgevingen.
- Autoriseer per veld in de resolvers, niet alleen op query- of mutatieniveau.
- Neem velden die nooit naar buiten mogen helemaal niet op in het schema.
- Begrens de diepte en de complexiteit van queries, en beperk het aantal resultaten per niveau.
- Geef neutrale foutmeldingen en schakel suggesties voor gelijkende veldnamen uit.
- Stel een snelheidsbeperking in die rekening houdt met de kosten van een query, niet alleen met het aantal verzoeken.
- Controleer geneste relaties, omdat die een omweg kunnen vormen langs een beschermd hoofdveld.
- Pas dezelfde autorisatie toe op mutaties als op queries.
- Beschouw uw schema als publieke informatie en laat de beveiliging niet op onbekendheid rusten.
Bronnen
Veelgestelde vragen
Is het uitschakelen van introspectie voldoende?
Nee, het is hooguit een drempel. Het schema kan ook worden afgeleid uit de JavaScript-bundel van uw frontend, uit foutmeldingen die veldsuggesties doen, en door systematisch veldnamen te proberen. De echte maatregel is autorisatie per veld; het uitschakelen maakt verkennen alleen lastiger.
Waarom is autorisatie per veld nodig?
Omdat de client bij GraphQL zelf samenstelt welke velden hij wil. Een controle op queryniveau zegt niets over de vraag of dit specifieke veld voor deze gebruiker bedoeld is. Staat een gevoelig veld in het schema zonder eigen controle, dan is het opvraagbaar, ook al vraagt uw frontend er nooit om.
Wat is een diepteaanval?
Een query die via relaties diep genest wordt opgebouwd, bijvoorbeeld auteur naar berichten naar auteur en zo verder. Zonder begrenzing kan één verzoek de server enorm belasten. Stel daarom een maximale diepte en een complexiteitslimiet in, en beperk het aantal resultaten per niveau.
Moet ik foutmeldingen aanpassen?
Ja. GraphQL-implementaties doen bij een onbekend veld vaak een suggestie voor een gelijkende naam. Dat is behulpzaam tijdens ontwikkeling en een verkenningshulpmiddel in productie. Schakel die suggesties uit en geef neutrale foutmeldingen.
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-213A01:2021API-antwoorden bevatten te veel gegevensEen API die complete databaserecords teruggeeft en het filteren aan de frontend overlaat, lekt velden die niemand had mogen zien.
- 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-770A04:2021Geen snelheidsbeperking op de APIZonder snelheidsbeperking kan een API onbeperkt worden bevraagd. Lees hoe dat leidt tot datadiefstal, misbruik van kosten en uitval.