Secure by Design - Hoofdstuk 4

Ingediend door Dirk Hornstra op 11-may-2020 08:20

In januari zijn we gestart met hoofdstuk 1 (link), in februari met hoofdstuk 2 (link), in april kwamen we bij hoofdstuk 3 (link) waarbij we volgens het boek "nu echt de diepte in zouden gaan". Nu vind ik dat nog wel meevallen (of tegenvallen), het is voornamelijk het uitleggen van begrippen en hoe zaken zouden moeten werken. Ik zie liever concrete voorbeelden met uitleg wat er fout gaat en hoe het anders kan. Het lijkt erop dat hoofdstuk 4 dat wel gaat bieden.

In het intro wordt nog even gezegd dat we onder tijdsdruk/deadlines misschien nog wel eens een "short-cut" gebruiken, we bepalen welk algoritme gebruikt wordt, hoe de flow in de applicatie loopt. Of we nu wel of geen tijdsdruk hebben, een hacker van jouw systeem maakt zich daar niet druk om, die vindt gewoon de zwakke plekken.

De Timo-samenvatting

De samenvatting die ik maak is meestal 1-op-1 een vertaling van het hoofdstuk, waarbij ik zoveel mogelijk overbodige zaken weg haal en de boel probeer te versimpelen. Maar als tijdens het overleg een hoofdstuk behandeld wordt geef ik meestal een paar kernwoorden die het hoofdstuk omschrijven. Laat ik dat hier dan ook meteen in het begin doen:

  • immutable: als je objecten in je code hebt, probeer ze zoveel mogelijk niet wijzigbaar te maken. En als een object dat wel moet zijn, probeer dat dan op een centrale plaats te doen.
  • contacten: publieke methoden zou je moeten "beveiligen" met pre- en post-conditions, zodat duidelijk is wat de input en output is. En ook geen aannames gedaan worden: "de aanroepende functie zou toch checken op NULL-waardes?"
  • vaste regels voor variabelen: plaats deze in de constructor, zodat de validatie meteen bij aanmaken uitgevoerd wordt.
  • voer validaties uit voordat een fout kan optreden. Als je een item uit een lijst gaat halen, controleer dan eerst of die lijst niet leeg is.
  • validaties op invoer:
  1. oorsprong
  2. grootte
  3. lexicale inhoud
  4. syntax
  5. semantiek

Hier hou ik van, een lijstje wat je naast je code kunt houden en afvinken. Voor de exacte uitleg, gewoon naar beneden scrollen. En wil je de iets uitgebreidere samenvatting, dan nu gewoon doorlezen.

Immutability

Als je een object in je code gebruikt moet je beslissen of het een "mutable" of een "immutable" object is (aanpasbaar of niet aanpasbaar). Niet aanpasbare objecten kun je veilig tussen threads delen en zorgen voor een hoge beschikbaarheid, heel belangrijk als je je systeem tegen DDOS-aanvallen wilt beschermen. Vaak zijn objecten aanpasbaar omdat het "makkelijker" is of omdat het "standaard in het framework zit".

Om dit toe te lichten krijgen we een voorbeeld van een webshop. Klanten kunnen producten in hun winkelwagen plaatsen. Elke klant heeft een credit-score die afhankelijk is van bestelgeschiedenis en klant-punten. Iedereen kan met een credit-card betalen, maar als je een hoge score hebt (betrouwbare klant bent) mag je ook op factuur betalen. Na een campagne treden er problemen op, veel klanten klagen over slechte response-tijden en ook blijken veel klanten met een lage score op factuur betaald te hebben. Hoe dan? De oorzaak blijkt in het Customer-object te liggen. We zien dat hier een aantal "getter" en "setters" in deze class zitten die de creditScore instellen en opvragen en een functie om te bepalen of deze klant op factuur mag betalen. We krijgen de 3 problemen:

  • lang wachten en slechte performance, categorie "beschikbaarheid", oorzaak: het systeem kan de gegevens niet tijdig opvragen en geeft een time-out
  • time-out bij orders tijdens het check-out proces, categorie "beschikbaarheid", oorzaak: het systeem kan de gegevens niet opvragen die nodig zijn om de order tijdig te verwerken
  • niet consistente betaalmogelijkheden, categorie "integriteit", oorzaak: de credit-score wordt op een ongeldige manier aangepast


Door impliciet zaken te blokkeren kun je de beschikbaarheid lager maken. 
De code die getoond werd had als eigenschap bij alle functies "synchronized". Dat zorgt ervoor dat er maar één thread toegang heeft. Maar omdat bepaalde processen parallel lopen, beginnen ze elkaar te blokkeren. In het volgende voorbeeld zijn de eigenschappen "final" geworden en worden de eigenschappen direct in de constructor ingesteld. Hierdoor kan het object veilig tussen theads gedeeld worden: no locking, no blocking. 

Maar je moet nog wel klantgegevens aanpassen. Hoe doe je dat als het object "immutable" is? Hiervoor moet je zaken splitsen via "kanalen". Dit lijkt de boel (onnodig) complex te maken, maar als je systeem een verschil heeft tussen het aantal reads en aantal writes loont het vaak de moeite. Dit komt terug in hoofdstuk 7, met het Entity Snapshot Pattern.

Hoe kan het dat er problemen waren? Dit wordt als volgt uitgewerkt:

  • de score kan op elk moment aangepast worden
  • de functie getCreditScore geeft het object terug, wat door de aanroepende code aangepast zou kunnen worden
  • de functie setCreditScore maakt geen kopie van het argument, maar geeft het argument door aan het interne object


De setCreditScore functie zou maximaal 1x aangeroepen moeten worden, maar dat is niet af te dwingen. En met de functie setId kun je een aangepaste customer-ID instellen, wat een ander resultaat terug kan geven dan de klant die nu zijn/haar bestelling probeert te plaatsen. 

Als beter alternatief wordt in de constructor van de class CreditScore de score meegegeven, éénmaal ingesteld, niet wijzigbaar dus. 

Het laatste punt in de opsomming van hierboven geeft aan dat een gedeeld creditScore-object in de code gebruikt wordt voor alle klanten: dan heb je dus een "integrity issue".

Wat nu exact de oorzaak was is niet van belang. De beslissing om de "customer" en "creditscore" "mutable" te maken heeft de code een stuk minder veilig gemaakt. 

Het gebruik van "contracten"

We gaan door met pre-conditions, contracten en post-conditions. In een real-life voorbeeld in het boek gaat dat om de loodgieter bij je kapotte douche, als jij zorgt dat de deur open is en het water afgesloten is (pre-conditions) zorgt hij dat de douche het weer doet (post-condition). Design contracts voor objecten werken op dezelfde manier. Ze specificeren de pre-conditions die nodig zijn om de functie te laten werken zoals deze bedoeld is en het specificeert de post-conditions hoe het object na verwerking veranderd is. In het boek wordt het voorbeeld gegeven van een kattenfokker. Deze heeft een lijst met kattennamen en heeft dat omgezet in een stukje software. De class CatNameList heeft de functie queueCatName om een naam toe te voegen, functie nextCatName geeft de eerstvolgende naam terug, de functie deQueueCatName verwijdert de oudste naam uit de lijst en met de functie size krijg je terug hoeveel namen we nog beschikbaar hebben. Qua clean-code vind ik CatNameList niet goed gekozen, want het principe lijkt meer op een Queue, maar goed....

We zien het contract wat hiervoor geldt. nextCatName: er moet wat in de lijst staan (pre-condition), geldt ook voor dequeuCatName. queueCatName mag geen null als naam krijgen en de naam moet een s bevatten. De post-conditions erbij zijn dat bij nextCatName de size gelijk blijft, bij deQueueCatName de size met 1 afneemt, en bij addCatName de size met 1 toeneemt. Je ziet dus dat een contract niet betrekking heeft op 1 functie maar op de hele class.

Veel security-gerelateerde zaken ontstaan doordat de aanname gedaan wordt dat de aanroepende code of aangeroepen code zaken afhandelt. Want in bovenstaande voorbeeld, wie voorkomt nu dat er dubbele katnamen zijn? Houdt de class CatNameList dat bij of is dat de verantwoordelijkheid van de mensen zelf?

Als de pre-conditions niet voldoen moet je een fail-fast doen. Dus niet een try-catch en lekker doormodderen, want in het geval van de loodgieter, als het water niet afgesloten is en hij gaat een half uur lang proberen de boel te fixen, terwijl het water uit de douche de gang oploopt, dan is de waterschade erger dan het originele probleem.

Om contracten robuust te maken moet je ze afdwingen in code. De programmeertaal Eiffel, ontworpen door Betrand Meyer ondersteunt dit. Bij andere programmeertalen zul je hier zelf moeite voor moeten doen. In het begin van je functie valideer je dus eerst de argumenten. Voldoen deze niet aan het contract, dan throw je een exceptie.

In het boek wordt gezegd dat je wel moet kijken naar verhouding inspanning en "de moeite waard zijn". Publieke methoden zou je zo moeten implementeren (in ieder geval controle op null waardes), interne helper-functies zou je kunnen overslaan.

De invariants in constructors

Bepaalde variabelen zijn altijd waar voor een object. Deze moeten ingesteld worden in de constructor, de zogenoemde invariants. In het voorbeed van CatNameList wordt in de constructor ingesteld dat er altijd een naam opgegeven moet worden (mag niet null zijn) en ook dat er een geslacht aangegeven is. Ook wordt ingesteld dat de naam een s moet bevatten. Alleen, nu hebben we op 2 plekken die validaties en dat botst met het DRY (don't repeat yourself) principe. In hoofdstuk 5 komen we hierop terug, de naam wordt dan een domain primite, een eigen class catName met zijn eigen validaties.

Falen / afsterven op het moment van een foutieve staat

De functie nextCatName geeft een waarde in de lijst. Maar als de lijst leeg is, zou dit een error opleveren. Met een validState-validatie of de lijst met namen niet leeg is wordt het opvragen uitgevoerd. In onze testprojecten gebruiken we nog wel eens Assert- om zaken te controleren. Kan dit ook in onze gewone code toegevoegd worden?

Validatie

OWASP benadrukt dat je ingevoerde data meteen moet valideren of het wel geldig is. Validatie is een wijd begrip. Voor de één kan ordernummer VZ3988 een goed ordernummer zijn, omdat dit aan de opmaak voldoet. Voor een ander kan dit niet voldoen, omdat in het ERP-systeem deze order niet bekend is. In het boek is een lijst opgesteld om stap voor stap de validatie door te lopen:

  • oorsprong: is de data afkomstig van een legitieme bron?
  • grootte: is de grootte te verklaren?
  • lexicale inhoud: bevat de inhoud de juiste tekens en encoding?
  • syntax: klopt het format?
  • semantiek: "slaat het ergens op"?

Oorsprong en grootte zijn meestal snel te controleren. Lexicale inhoud heeft al wat meer acties nodig (regex o.i.d.?). Voor de syntax moet je de data misschien parsen, wat een zwaardere belasting is. En voor de semantiek moet je misschien aanvullende database-controles doen. De lijst is daarom in deze volgorde opgezet.

Oorsprong van de data

Botnets vuren soms zoveel ongeldige data af op een systeem, dat deze zo druk is met het valideren, dat geldige calls niet meer verwerkt kunnen worden (een denial of service, DOS). Als vanaf meerdere locaties dat gebeurt spreek je over een distributed Dos, DDOS. Je kunt valideren op oorsprong, op basis van IP adres en op basis van een mee gegeven API key. Bepaalde zaken zijn beschikbaar voor de buitenwereld en zullen dus geen restricties hebben. Maar andere zaken in je netwerk, die alleen vanaf bepaalde servers aangeroepen kunnen/mogen worden kun je wel op basis van IP-adres afschermen. Met oAuth kun je een accesstoken gebruiken, eerder heeft een vertrouwd persoon zijn/haar toestemming gegeven en is dat token aan hem/haar gekoppeld. Als verzoeken met ongeldige tokens (of zonder tokens) binnenkomen, kun je die blokkeren.

Controleer de grootte van de data

Als iemand een video upload, kan een request tussen de 100 MB en 1 GB zitten. Maar als er via JSON een order wordt ingeschoten, dan zal het ordernummer niet 1GB groot zijn. Als je een combinatie van beide binnen krijgt, een JSON-bestand met een ordernummer, maar ook een BASE64-coded versie van de video, is de validatie van de totale grootte niet voldoende, je moet dan ook per veld controleren of de grootte redelijk is. 1GB  voor een ordernummer is dat zeker niet! Zo kun je een ISBN-nummer hebben, voordat je er een regex op uitvoert zou je eerst kunnen controleren of de lengte voldoet aan de normale lengte van dit type veld. Voor je een regex laat stampen op 1GB aan cijfers in een string, kun je al snel een IllegalArgumentException opgooien als de lengte groter is dan 10 karakters.

Lexicale inhoud

Het verwerken van JSON of XML is een intensief proces. Daarom proberen we voor de tijd de data te beoordelen. Is de volledige content HTML-encoded? Dan mag er geen < of > meer in voorkomen. Staat dit er wel in: meteen stoppen met verwerken. Als je bron gecompliceerder is, XML bijvoorbeeld, dan zou je een lexer/tokenizer moeten gebruiken. Deze splitst de data naar lexemes/tokens. Je krijgt dan elke tag als een token, elke letter daarbinnen als een los token. We zien het voorbeeld wat we ook al eens bij clean code hebben gezien. Een relatief kort stukje XML bevat referenties naar zichzelf, waardoor als de waardes geïnterpreteerd worden de boel uit elkaar barst naar ruim een biljoen lol strings, iets wat je niet wilt. Door in te stellen dt je bepaalde tags niet verwerkt kun je jezelf daartegen beschermen.

Syntax

Veel validaties kun je met een Regex doen. Maar, zoals de auteur zegt: als je al hoofdpijn krijgt als je naar een regex krijgt, probeer die validatie dan in code uit te voeren. Een ISBN nummer is prima te doen. Voor XML heb je een parser nodig. Omdat dit intensief kan zijn , kijken we naar het principe van een "checksum". Zo heb je bij een ISBN nummer dat het laatste cijfer een controlegetal is.

Semantiek

Als je tot dt punt komt, dan heeft de data al veel controles doorstaan. Tot nu toe heb je alleen met de data zelf gewerkt, nog niet met de invloed van de data op het systeem (tot nu toe heb je waarschijnlijk geen database-interacties hoeven uitvoeren). Is het een order met een niet-bestaand productcode? Wil iemand wat toevoegen aan een order die al afgerond is (afgeleverd bij de klant?). Meteen afkappen. Voor de rest, omdat alle controles geweest zijn hoef je niet nogmaals te controleren of het ISBN-nummer wel klopt. Die controle heb je immers al uitgevoerd bij de syntax-controle.