Direct naar inhoud

Invoervalidatie ontbreekt op de server

CWE-20CWE-602OWASP A04:2021Bijgewerkt 3 september 20265 min leestijd

Controles die alleen in de browser plaatsvinden, verplichte velden, maximumlengtes, keuzelijsten, prijsberekeningen, zijn met een rechtstreeks verzoek volledig te omzeilen. De browser is een gemak voor de gebruiker, geen beveiliging. Elke waarde die de server binnenkrijgt, moet daar opnieuw en volledig worden gevalideerd.

Een formulier dat netjes waarschuwt bij een te lang veld en alleen bestaande opties in een keuzelijst toont, wekt de indruk dat de invoer onder controle is. Die indruk geldt alleen voor gebruikers die het formulier gebruiken. Wie het verzoek zelf opstelt, heeft met geen van die regels te maken. Hieronder leest u wat daaruit volgt en hoe u de controle terugbrengt naar de plek waar hij telt.

Waarom telt validatie in de browser niet?

Alles wat in de browser gebeurt, staat onder controle van de gebruiker. De HTML kan worden aangepast, de JavaScript kan worden uitgeschakeld of herschreven, en het verzoek kan volledig buiten de pagina om worden opgesteld. Ontbrekende invoervalidatie op de server betekent dat de applicatie vertrouwt op controles die in die omgeving plaatsvinden.

Het onderscheid dat helpt: de browser valideert voor gemak, de server valideert voor correctheid. Een maxlength op een invoerveld voorkomt dat een gebruiker per ongeluk te veel typt. Hij voorkomt niet dat iemand een verzoek stuurt met een waarde van tienduizend tekens. Beide controles mogen bestaan, maar alleen de tweede is een maatregel.

Denk aan een bestelformulier met vakjes van een vaste grootte. Wie het formulier invult, wordt door die vakjes gestuurd. Maar de bestelling wordt uiteindelijk verwerkt door iemand die alleen naar de ingevulde waarden kijkt, en die krijgt ook de bestelling te zien die op een eigen vel papier is geschreven, zonder vakjes.

Hoe omzeilt een aanvaller validatie in de browser?

Kwetsbaar:

<!-- De enige controle staat in het formulier -->
<form action="/bestellen" method="post">
  <input name="aantal" type="number" min="1" max="10" required>
  <input name="prijs" type="hidden" value="49.95">
  <select name="verzendwijze">
    <option value="standaard">Standaard</option>
    <option value="express">Express (+ 9,95)</option>
  </select>
  <button>Bestellen</button>
</form>
app.post('/bestellen', vereistLogin, async (req, res) => {
  const totaal = req.body.aantal * req.body.prijs;
  await bestellingen.maak({
    gebruiker: req.gebruiker.id,
    aantal: req.body.aantal,
    verzendwijze: req.body.verzendwijze,
    totaal,
  });
  res.send('Bedankt voor uw bestelling');
});

De server neemt alles over wat binnenkomt, inclusief de prijs uit een verborgen veld. Een verzoek buiten het formulier om is triviaal:

POST /bestellen HTTP/1.1
Content-Type: application/json

{"aantal": -5, "prijs": 0.01, "verzendwijze": "gratis-koerier"}

Er gebeuren nu drie dingen tegelijk. De prijs is bepaald door de klant. Het aantal is negatief, wat afhankelijk van de verwerking tot een creditering kan leiden. Ook de verzendwijze is een waarde die niet in de keuzelijst voorkwam en waarvan niemand weet hoe de rest van het systeem erop reageert.

Veilig:

import { z } from 'zod';

const Bestelling = z.object({
  artikelId: z.string().uuid(),
  aantal: z.number().int().min(1).max(10),
  verzendwijze: z.enum(['standaard', 'express']),
}).strict();                       // onbekende velden worden geweigerd

app.post('/bestellen', vereistLogin, async (req, res) => {
  const resultaat = Bestelling.safeParse(req.body);
  if (!resultaat.success) {
    return res.status(400).json({ fout: 'Ongeldige invoer' });
  }
  const { artikelId, aantal, verzendwijze } = resultaat.data;

  // Prijzen komen uit de eigen administratie, nooit uit het verzoek
  const artikel = await artikelen.zoek(artikelId);
  if (!artikel || !artikel.beschikbaar) return res.status(404).json({ fout: 'Onbekend artikel' });

  const totaal = artikel.prijs * aantal + VERZENDKOSTEN[verzendwijze];

  await bestellingen.maak({ gebruiker: req.gebruiker.id, artikelId, aantal, verzendwijze, totaal });
  res.json({ ok: true, totaal });
});

Het schema beschrijft de volledige vorm van het verzoek: welke velden er zijn, van welk type, binnen welke grenzen en met welke toegestane waarden. Met strict() worden onbekende velden geweigerd, wat meteen bescherming biedt tegen het meesturen van eigenschappen die niet bedoeld waren. En het belangrijkste: de prijs komt niet meer uit het verzoek maar uit uw eigen administratie. Gegevens die de gebruiker niet hoort te bepalen, hoort u ook niet van hem aan te nemen.

Valideer niet alleen de vorm maar ook de bedrijfsregels. Een aantal van 10 is een geldig getal en kan toch onmogelijk zijn wanneer er twee stuks op voorraad liggen. Een datum in het verleden is een geldige datum en toch geen geldige afspraak. Dat soort controles ontbreekt vaak, juist omdat het schema al “goed” zegt.

Wat is de impact van ontbrekende invoervalidatie?

De ernst loopt van middelzwaar tot hoog en wordt bepaald door wat er achter de ontbrekende controle gebeurt. Ontbrekende validatie is zelden zelf de kwetsbaarheid; ze is de voorwaarde waaronder andere kwetsbaarheden ontstaan.

Aan de technische kant is het de eerste stap in vrijwel elke injectieaanval. Waarden die zonder controle worden doorgegeven aan een database, een systeemaanroep, een sjabloon of een bestandspad, zijn precies wat SQL-injectie, commando-injectie en path traversal mogelijk maakt. Een te lange of onverwacht gevormde waarde kan bovendien tot een storing leiden waarin de applicatie meer prijsgeeft dan bedoeld.

Aan de bedrijfsmatige kant zijn de gevolgen concreter en soms directer meetbaar. Een prijs die door de klant wordt bepaald, een negatief aantal dat tot een terugbetaling leidt, een korting die op zichzelf wordt gestapeld, een status die naar “betaald” wordt gezet. Dat zijn geen technische kwetsbaarheden maar logische, en juist die worden door geautomatiseerde hulpmiddelen vrijwel nooit gevonden.

Hoe spoor je ontbrekende invoervalidatie op?

Een tester werkt niet via het formulier maar stuurt de verzoeken rechtstreeks. Voor elk veld wordt geprobeerd wat er gebeurt bij waarden die de interface niet toestaat: een negatief getal, een extreem groot getal, een tekst waar een getal wordt verwacht, een lege waarde, een ontbrekend veld, een waarde die niet in de keuzelijst stond.

Daarnaast wordt gelet op verborgen velden en op waarden die de client niet zou moeten bepalen: prijzen, kortingen, statussen, identificaties van eigenaren, rollen. Elk daarvan wordt gewijzigd om te zien of de server het overneemt. Ook worden extra velden meegestuurd die niet in het formulier voorkwamen, om te toetsen of ze stilzwijgend worden verwerkt. Verder wordt gekeken of de validatie op elk kanaal gelijk is: de webinterface en de API worden vaak apart geïmplementeerd, met verschillende strengheid. AssistSec besteedt daarbij bijzondere aandacht aan de bedrijfsregels, omdat die controles per applicatie verschillen en daarom niet door standaardhulpmiddelen worden gedekt.

Hoe voorkom je ontbrekende invoervalidatie?

  • Valideer elk verzoek aan serverzijde met een schema dat de volledige vorm beschrijft.
  • Weiger onbekende velden in plaats van ze te negeren.
  • Werk met toegestane waarden en bereiken, niet met een lijst van wat verboden is.
  • Neem prijzen, kortingen, statussen en rollen nooit over uit het verzoek; haal ze uit uw eigen administratie.
  • Valideer naast de vorm ook de bedrijfsregels: voorraad, geldigheid, samenhang tussen velden.
  • Voer de validatie op één plek per endpoint uit, zodat geen enkel pad eromheen kan.
  • Beschouw validatie in de browser als gebruiksgemak en herhaal alles op de server.
  • Escaping bij de uitvoer blijft nodig; validatie vervangt dat niet.
  • Pas dezelfde regels toe op API’s, webhooks en achtergrondverwerking, niet alleen op webformulieren.

Bronnen

Veelgestelde vragen

Moet ik dan alles dubbel valideren?

Ja, maar met een ander doel. In de browser valideert u voor het gebruiksgemak: directe terugkoppeling, minder verkeer, een prettiger formulier. Op de server valideert u voor de correctheid en de veiligheid. Dat de regels overlappen is geen verspilling; het zijn twee verschillende functies.

Is een toegestane lijst beter dan een verboden lijst?

Vrijwel altijd. Een lijst van wat niet mag, is per definitie onvolledig: er is altijd een variant waar niemand aan heeft gedacht. Een lijst van wat wél mag, sluit alles daarbuiten uit, ook wat u niet had voorzien. Dat verschil is precies waar filters op verboden tekens op stuklopen.

Vervangt validatie het escapen van uitvoer?

Nee, en die verwarring veroorzaakt kwetsbaarheden. Validatie bepaalt of een waarde acceptabel is bij binnenkomst; escapen zorgt dat een waarde veilig wordt verwerkt in de context waarin hij belandt. Een geldige naam als O'Brien moet worden toegelaten én correct worden verwerkt in een query.

Waar valideer ik het beste?

Zo vroeg mogelijk aan serverzijde, op één plek per endpoint, met een schema dat de vorm van het hele verzoek beschrijft. Dat is betrouwbaarder dan losse controles verspreid door de code, omdat er dan geen pad kan bestaan dat de validatie omzeilt.

Verwante artikelen

Druk op / om te zoeken · Esc