Direct naar inhoud

Geen snelheidsbeperking op de API

CWE-770CWE-799OWASP A04:2021Bijgewerkt 3 september 20265 min leestijd

Een API zonder snelheidsbeperking accepteert net zoveel verzoeken als een aanvaller wil versturen. Daarmee komt het uitputtend doorzoeken van gegevens, het geautomatiseerd raden van inloggegevens en het opjagen van uw verwerkingskosten mogelijk. Een limiet per gebruiker en per endpoint verandert een aanval van triviaal in onpraktisch.

Een API is ontworpen om machinaal te worden aangeroepen, en dat is precies waarom het ontbreken van een grens hier zwaarder weegt dan bij een webformulier. Wat voor een gebruiker een handeling van enkele seconden is, is voor een script een lus die duizenden keren per minuut draait. Hieronder leest u waar dat toe leidt en hoe u een limiet instelt die uw legitieme gebruikers niet in de weg zit.

Wat is snelheidsbeperking?

Snelheidsbeperking, in het Engels rate limiting, is het begrenzen van het aantal verzoeken dat een aanvrager binnen een bepaalde tijd mag doen. Wordt die grens overschreden, dan antwoordt de server met een 429 Too Many Requests in plaats van het verzoek uit te voeren.

Het doel is niet het weren van misbruik in de zin van kwaadaardige invoer, daar zijn andere maatregelen voor. Het doel is het verstoren van de economie van een aanval. Vrijwel elke geautomatiseerde aanval berust op herhaling: duizenden wachtwoorden proberen, honderdduizenden identificaties aflopen, een lijst met e-mailadressen verifiëren. Zolang die herhaling gratis en onbeperkt is, is de aanval eenvoudig. Zodra elke poging tijd kost, verschuift de rekensom.

Vergelijk het met een balie waar iemand vragen mag stellen. Eén vraag per keer is normaal gebruik. Tienduizend vragen per uur is geen gebruik meer maar een systematische doorzoeking, en het is redelijk om daar een grens aan te stellen, ook al is elke vraag afzonderlijk volkomen legitiem.

Hoe wordt een ontbrekende snelheidsbeperking misbruikt?

Kwetsbaar:

// Elk verzoek wordt uitgevoerd, hoeveel het er ook zijn
app.post('/api/v1/kortingscode/controleer', async (req, res) => {
  const code = await codes.zoek(req.body.code);
  res.json({ geldig: Boolean(code), waarde: code?.waarde ?? null });
});

Functioneel is hier niets mis mee. Maar een aanvaller ziet een orakel dat hem per verzoek vertelt of een code bestaat, en niets houdt hem tegen om dat systematisch af te lopen:

POST /api/v1/kortingscode/controleer   {"code":"ZOMER-0001"}  → ongeldig
POST /api/v1/kortingscode/controleer   {"code":"ZOMER-0002"}  → ongeldig
...
POST /api/v1/kortingscode/controleer   {"code":"ZOMER-4471"}  → geldig, 25 euro

Bij duizend verzoeken per seconde is de hele reeks binnen een uur doorlopen. Hetzelfde patroon geldt voor inlogpogingen, voor het aflopen van klantnummers en voor het verifiëren van welke e-mailadressen bij u bekend zijn.

Veilig:

import { RateLimiterRedis } from 'rate-limiter-flexible';

const limiet = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'code-check',
  points: 20,               // 20 verzoeken
  duration: 60,             // per minuut
  blockDuration: 300,       // daarna 5 minuten geblokkeerd
});

app.post('/api/v1/kortingscode/controleer', async (req, res) => {
  const sleutel = req.gebruiker?.id ?? req.ip;   // account boven IP-adres

  try {
    const status = await limiet.consume(sleutel);
    res.set('RateLimit-Remaining', String(status.remainingPoints));
  } catch (over) {
    res.set('Retry-After', String(Math.ceil(over.msBeforeNext / 1000)));
    return res.status(429).json({ fout: 'Te veel verzoeken' });
  }

  const code = await codes.zoek(req.body.code);
  res.json({ geldig: Boolean(code), waarde: code?.waarde ?? null });
});

De limiet wordt bijgehouden in een gedeelde opslag, zodat hij ook klopt wanneer er meerdere servers achter een loadbalancer draaien: een limiet in het geheugen van één proces is met een paar verzoeken te omzeilen. De sleutel is bij voorkeur het account, omdat een aanvaller dat niet zomaar kan wisselen, met het IP-adres als terugval voor anonieme verzoeken. De Retry-After-header vertelt legitieme clients netjes wanneer ze het opnieuw mogen proberen.

Let op de manier waarop u het IP-adres bepaalt. Achter een proxy of CDN staat het echte adres in een header als X-Forwarded-For, en die kan een aanvaller zelf meesturen. Vertrouwt u die waarde zonder te controleren welke proxy hem heeft gezet, dan omzeilt hij uw limiet door bij elk verzoek een ander adres op te geven.

Wat is de impact van een ontbrekende snelheidsbeperking?

De ernst wordt doorgaans als middelzwaar beoordeeld, omdat het ontbreken van een limiet zelden op zichzelf tot compromittering leidt. De betekenis zit in de aanvallen die erdoor haalbaar worden.

De eerste categorie is het uitputtend doorzoeken van gegevens. Bestaat er een endpoint dat op basis van een identificatie iets teruggeeft, dan kan een aanvaller zonder limiet de hele reeks aflopen en zo een database naar buiten kopiëren, verzoek voor verzoek, allemaal keurig geautoriseerd. In combinatie met te ruime autorisatie op objectniveau is dat een van de meest voorkomende oorzaken van grootschalige datalekken bij API’s.

De tweede is het raden van geheimen: wachtwoorden, eenmalige codes, herstel-tokens en kortingscodes. Een tweefactorcode van zes cijfers kent een miljoen mogelijkheden, wat zonder limiet binnen bereik ligt en met een limiet van vijf pogingen volstrekt onhaalbaar is.

De derde is minder bekend maar in de praktijk kostbaar: het opjagen van uw verwerkingskosten. Endpoints die een zware zoekopdracht uitvoeren, een PDF genereren, een e-mail of sms versturen of een betaalde dienst van derden aanroepen, kosten geld per aanroep. Zonder limiet kan een aanvaller die rekening laten oplopen, of uw dienst simpelweg overbelasten.

Hoe spoor je een ontbrekende snelheidsbeperking op?

Een tester stuurt een reeks verzoeken in hoog tempo naar de gevoeligste endpoints (inloggen, wachtwoordherstel, zoekfuncties, endpoints die op identificatie opvragen) en kijkt of er een punt komt waarop de server begint te weigeren. Blijft alles doorgaan, dan ontbreekt de limiet.

Daarna wordt geprobeerd de limiet te omzeilen, want die is er vaker wel dan hij werkt. Klassieke omwegen zijn het wisselen van IP-adres via een header als X-Forwarded-For, het variëren van hoofdlettergebruik of afsluitende schuine strepen in het pad, het wisselen tussen verschillende API-versies die naar dezelfde functionaliteit leiden, en het spreiden van verzoeken over meerdere accounts. Ook wordt gecontroleerd of de limiet in een gedeelde opslag wordt bijgehouden of per server, en of hij ook op de API geldt en niet alleen op de webinterface. AssistSec toetst daarnaast of de weigering zelf niets prijsgeeft: een limiet die alleen optreedt bij bestaande accounts, verraadt precies welke accounts bestaan.

Hoe voorkom je een ontbrekende snelheidsbeperking?

  • Stel een limiet in per endpoint, afgestemd op de kosten en de gevoeligheid ervan.
  • Baseer de limiet op het account waar dat kan, en op het IP-adres voor anonieme verzoeken.
  • Houd de tellers bij in een gedeelde opslag, zodat de limiet klopt over meerdere servers heen.
  • Antwoord met 429 en een Retry-After-header, zodat legitieme clients zich kunnen aanpassen.
  • Hanteer strengere limieten op inloggen, wachtwoordherstel, verificatiecodes en zware zoekopdrachten.
  • Bepaal het IP-adres alleen uit headers die door uw eigen vertrouwde proxy zijn gezet.
  • Voer de limiet eerst in waarschuwingsmodus in, meet het werkelijke gebruik en handhaaf daarna.
  • Begrens ook de omvang van antwoorden met paginering en een maximum aantal resultaten per verzoek.
  • Bewaak en alarmeer op patronen die op systematisch doorzoeken wijzen, ook wanneer de limiet niet wordt bereikt.

Bronnen

Veelgestelde vragen

Waarop moet ik de limiet baseren?

Op de meest betrouwbare identificatie die u hebt. Voor ingelogde gebruikers is dat het account, want dat kan een aanvaller niet zomaar wisselen. Voor anonieme verzoeken is het IP-adres het enige aanknopingspunt, met de kanttekening dat gebruikers achter één kantoornetwerk dat adres delen. Combineer beide waar mogelijk.

Is één globale limiet niet genoeg?

Nee, want endpoints verschillen sterk in kosten en gevoeligheid. Een inlogendpoint of een zoekfunctie die de database zwaar belast verdient een veel strengere limiet dan het ophalen van een statische lijst. Eén ruime limiet voor alles is meestal te laks waar het ertoe doet.

Hoe voorkom ik dat legitieme gebruikers hinder ondervinden?

Meet eerst hoe uw API in de praktijk wordt gebruikt en leg de limiet ruim boven het normale piekgebruik. Voer hem daarna eerst in waarschuwingsmodus in, zodat u overschrijdingen ziet zonder iets te blokkeren, en pas hem aan voordat u gaat handhaven.

Helpt een limiet tegen een gedistribueerde aanval?

Slechts gedeeltelijk. Een aanvaller die duizenden IP-adressen inzet, omzeilt een limiet per adres. Daarom is een limiet per account belangrijk, en zijn aanvullende maatregelen nodig op netwerkniveau, bijvoorbeeld bij uw CDN of dienstverlener.

Verwante artikelen

Druk op / om te zoeken · Esc