Secure by Design - Hoofdstuk 5

Ingediend door Dirk Hornstra op 04-jun-2020 20:30

In januari zijn we gestart met hoofdstuk 1 (link), in februari met hoofdstuk 2 (link), in april met hoofdstuk 3 (link), in mei met hoofdstuk 4 (link) en nu wordt het tijd voor het 5e hoofdstuk.

De titel van hoofdstuk 5 is "Domain primitives".

We kijken nog even terug naar hoofdstuk 4, daar hebben we op basis van immutability (onveranderlijk), failing fast (snel stoppen) en validatie gezorgd dat onze code een stuk veiliger wordt. Hoewel dit goede acties zijn, zegt dit hoofdstuk dat het los toepassen van deze acties niet een effectieve manier is om veilige code te bouwen. Dit hoofdstuk bestaat uit een 3-tal secties om dat aan te pakken.

De Timo-samenvatting

De korte samenvatting van dit hoofdstuk;

  • Probeer je variabelen zoveel mogelijk om te zetten naar domain primitives. Heeft je class Account een veld Password wat een "string" is, dan is dat een uitgelezen mogelijkheid om dat password een "domain primitive" te maken. Want een wachtwoord heeft een vereiste minimale lengte. Een bepaalde structuur (moet een hoofdletter bevatten, vreemd teken). Mag niet in een standaard dictionary voorkomen. En wat je allemaal nog meer kunt bedenken. Nu zul je waarschijnlijk al die "intelligentie" in de Account-class hebben. Maar stel dat er een nieuwe API interface komt die ook een ander type accounts gaat aanmaken. Dat wachtwoord moet nog steeds aan dezelfde eisen voldoen. Door een eigenschap om te zetten naar een object, zorg je dat deze "verantwoordelijk" wordt voor zichzelf en je dat niet op tig plaatsen in code hoeft te controleren en af te dwingen (of waarschijnlijk ooit een keer zult vergeten...).
  • Valideer goed of je jouw domain primitive een goede definitie geeft. Als je een rekeningnummer-eigenschap hebt, maar deze mag ook leeg zijn, geef het dan niet de naam "IBAN", omdat je dan mensen de indruk geeft dat er altijd een waarde ingevuld moet zijn en je dus ook altijd het controlegetal kunt berekenen. Maak dan een domain primitive "BankAccount" aan die een IBAN-eigenschap heeft, maar ook een boolean isEmpty-eigenschap heeft.
  • De opzet van "read-once-objecten" is ook een prima oplossing voor dit wachtwoord. Want als je het wachtwoord opvraagt voor een controle, zal dat op 1 plek gedaan worden (is invoer gelijk aan het opgeslagen wachtwoord?). Als het wachtwoord nog een keer wordt gelezen heb je een mogelijk security-issue, want dan is "iets" of "iemand anders" de waarde aan het uitlezen. Mogelijk is er een fout opgetreden en wordt via bijvoorbeeld Elmah-logging de gegevens opgeslagen. Maar deze waarde wil je daar absoluut niet zien. Met read-once-objecten zorg je dat ze maar 1 maal te lezen zijn en ook dat ze niet serialiseerbaar zijn.


Dit zijn wat mij betreft de belangrijkste eye-openers van dit hoofdstuk. Als je zelf nog verder wilt lezen, ga je gang!

Domain primitives en invarianten
In Domein Driven Design wordt bepaald dat value-objects onveranderlijk zijn en één geheel vormen. Als je dit onthoudt en wat tweaks toevoegt kun je er een domain primitive van maken. Als je deze als kleinste waardes van je code bouwt, neemt de waarschijnlijkheid af dat er veiligheidslekken ontstaan.

Wat zijn domain primitives?
Als je een variabele in je code introduceert bepaal je de naam, wat voor object het moet weergeven, wat het is en wat het zeker niet is. Hiermee kun je afdwingen dat tijdens het aanmaken aan die regels voldaan wordt. Als de representatie niet geldig is, dan bestaat die niet. Dit type variabele is een domain primitive.
Domain primitives, het klinkt als een "primitief type", dus je denkt aan een int, string, maar dat is dus niet zo. De objecten kunnen behoorlijk complex zijn, andere domain primitives bevatten en uitgebreide logica bevatten. Tevens verbied je het gebruik van simpele "primitives" en generieke types (ook NULL).

We gaan naar een voorbeeld. Stel dat het concept "aantal" onderdeel uitmaakt van jouw domain model. Het is het aantal producten dat een klant wil kopen in jouw webshop. Het is een nummer, maar je maakt een domain primitive Quantity aan. Tijdens overleg met de domain experts kom je erachter dat 0 of lager geen geldige waarde is en een geldig aantal tussen de 1 en 200 ligt. De functionaliteit om producten toe te voegen komt ook in deze domain primitive en niet "extern" in een ander stuk code. Want stel dat er bepaalde regels zijn dat bij 5 producten er automatisch een bonusproduct toegevoegd wordt, dan moet je die logica centraal houden.

Context boundaries (grenzen van de context) geven betekenis aan de code
Domain primitives worden gedefinieerd door hun waarde, niet door één of ander identity-veld. Dit betekent dat 2 domain primitives van hetzelfde type en die dezelfde waarde hebben uitwisselbaar zijn. Als je een concept maakt voor een domain primitive, dan moet dit voldoen aan het domein waarin het actief is.

We gaan naar een voorbeeld. Je hebt een systeem gemaakt waarin gebruikers een eigen e-mailadres kunnen aanvragen. De gebruiker kiest het deel voor de @ en als deze gemaakt is kunnen ze mail versturen en ontvangen. Als je dit maakt zie je dat het e-mailadres een prima kandidaat is om er een domain primitive van te maken. Voor e-mailadressen gelden regels en die kun je op deze manier afdwingen. Hoewel je de RFC 3696 kunt volgen en als de standaard kunt instellen, is dat niet van toepassing binnen het huidige domein. Het kan zijn dat bepaalde e-mailadressen niet mogen (root, admin), maar volgens de RFC zijn dat geldige mailadressen.

Een ander voorbeeld is het ISBN-nummer. ISO heeft hier een standaard voor opgesteld. Als je een soort kopie daarvan in jouw code gaat bouwen, voeg je de mogelijkheid toe dat er misverstanden, aannames en bugs geïntroduceerd worden. Als je een reeds bekende term in jouw code gaat verwerken, komt dat vaak doordat de term gebruikt moet worden om meer dan één ding in jouw context te beschrijven. Probeer dit dan op te splitsen of kom met een volledig nieuwe term.
In het voorbeeld van het ISBN-nummer blijkt dat in jouw systeem ook boeken zijn die nog geen ISBN-nummer gekregen hebben. Een aanpak zou kunnen zijn om de term ISBN een andere betekenis te geven, zodat ook die boeken verwerkt kunnen worden. Met een eigen prefix. Maar je hoort het al, dit klinkt al "buggy". Een betere manier zijn om een eigen term te maken, BookId, die een ISBN of een UnpublishedBookNumber bevat. Nu weet je tenminste of je een boek met of een boek zonder een nummer hebt.

Probeer zoveel mogelijk je variabelen op deze manier te bouwen, waardoor je een bibliotheek met domain primitives krijgt.

API's robuuster maken met jouw bibliotheek van domain primitives

Op deze manier heb je altijd invoer- en uitvoervalidatie, waardoor het risico van beveiligingslekken door foutieve invoer wordt verminderd.

Er volgt een voorbeeld. Je krijgt de taak om de audit log-bestanden van een systeem naar een centrale repository te sturen. Omdat deze logbestanden gevoelige informatie bevatten mogen deze niet naar een verkeerde locatie gestuurd worden! We zien het korte codevoorbeeld: void sendAuditLogsToServerAt(string ipAdres).

Oei. In principe kun je nu naar elk ip-adres de logs sturen. Ook geen validatie op poort (via HTTP of HTPPS), dus is je transport onderweg nog te onderscheppen? Je maakt (dus) een domain primitive InternalAddress. In die domain primitive kun je nu gaan configureren welke ip-adressen geldig zijn en wat voor extra beperkingen je wilt toevoegen.

Voorkom dat jouw interne domein publiekelijk zichtbaar wordt

Als je een REST API bouwt en de domein-objecten één op één overzet, maak je het jezelf moeilijk. Je kunt je interne domein niet aanpassen, want dan moeten de gebruikers ook hun cliënt aanpassen. En dan is de cliënt die de meeste moeite heeft om dat bij te kunnen werken de beperkende factor, niet alleen voor jou, maar ook voor de andere clients. Wat je wilt is een andere weergave van jouw domeinobjecten. Dit kan met een Data Transfer Object (DTO). Het eerste wat je doet is deze DTO objecten om te zetten naar domain primitives. Door dit concept ontkoppel je de api en je interne domein.

De samenvatting:

  • de invariants van domain primitives (de representaties) worden gecontroleerd tijdens het aanmaken
  • alleen geldige domain primitives kunnen bestaan
  • gebruik altijd domain primitives en geen standaard string, bool, etc.
  • de intentie en de wijze van gebruik is binnen de grenzen gedefinieerd, ook als de term in een ander domein voorkomt
  • maak er een bibliotheek van om zo veilige code te maken

 

Read-once objects

Het lekken van gevoelige informatie kan ontstaan door een fout van de ontwikkelaar (bug) of door een met opzet geprepareerde actie (hack). Hoe kunnen 1x-lezen-objecten hierbij helpen?

  • de hoofdreden is om oneigenlijk gebruik te detecteren
  • het is een weergave van gevoelige informatie of concept
  • het is vaak een domain primitive
  • de waarde kan maar 1x gelezen worden
  • het voorkomt serialisatie van gevoelige data
  • het voorkomt het maken van subclasses en extensies


Read-once objecten gebruik je voor paspoortnummers, creditcard nummers of wachtwoorden. We krijgen een voorbeeld van een class in Java. De class kan niet overgeërfd worden (final, in C# is dat sealed), het veld voor de gevoelige data is AtomicReference, een transient field (waardoor je het niet kun serialiseren). Volgens StackOverflow is het attribuut [ScriptIgnore] de C# variant hiervan.

Detecteer oneigenlijk gebruik

We zien een voorbeeld, een standaard inlogscherm met gebruikersnaam en wachtwoord. De validatie wordt door een extern systeem gedaan, de enige keer dat je het ingevoerde wachtwoord nodig hebt is het moment dat je het doorgeeft naar het authenticatiesysteem. Niet meer, niet minder. Wachtwoorden zijn typische gegevens die je niet in plain-tekst in een logbestand terug wilt zien. We zien een stuk voorbeeldcode, bij de invoer (het opslaan van invoer) wordt de invoer gekloond. Bij het opvragen van de data wordt het interne veld gekloond, wordt de waarde leeg gemaakt, boolean "consumed" op true gezet en dit binnen een synchronized functie, zodat er maar 1 item toegang heeft. In dit stuk javascript-code wordt gebruik gemaakt van een array van karakters in plaats van een string. In C# heb je in de namespace System.Security een SecureString-class die voor ons waarschijnlijk interessant is.

Voorkomt het lekken van informatie door code die zich ontwikkelt

We krijgen een voorbeeld van de auteurs van het boek. Er is een web-applicatie gebouwd. Omdat er vaak data nodig is van de ingelogde gebruiker is het besluit genomen om het User-object in de sessie op te slaan. Werkt prima. Echter, later komt in het User-object het veld BSN-nummer bij, een read-once-object. De testen gaan allemaal goed, software lijkt nog te werken. Let wel, het is een Java-web-applicatie. Tomcat wordt als webserver gebruikt, het valt de ontwikkelaar op dat bij het afsluiten van de applicatie er stack-traces getoond worden. Dan blijkt dat Tomcat sessies op schijf wil opslaan en dat dus ook doet bij dit veld wat niet geserialiseerd mag en kan worden...

Domain primitives, de basis voor de rest van de code

Als er geen domain primitives zouden zijn, zou de overige code zich moeten richten op validatie, formattering (weergave), vergelijken en nog veel meer. Entities zijn een weergave van objecten uit het echte leven, de winkelwagen, de kamers van een hotel. Als deze met int en string-waardes gaan werken, worden deze entities opgezadeld met alle controles en bijkomende functionaliteit in plaats van zich bezig houden waar ze voor bedoeld zijn.

Het risico van entity methoden die vol met code zitten (overcluttered)

We zien het voorbeeld van de entity Order van een boekwinkel. We zien de functie addItem om een boek aan je order toe te voegen. Tien regels met business-rules. In dit stuk code missen 2 controles, de checksum van een ISBN nummer waarmee je controleert of deze geldig is en negatieve aantallen. En er was nog ergens een beperking dat er maximaal 240 boeken in één bestelling kunnen, die controle zit er ook niet in. En er missen nog acties. Als het boek al in de order zit, wil je een nieuwe regel toevoegen, of wordt het aantal in je order dan 2?

We krijgen de theorie (die waarschijnlijk wel klopt) dat de mens goed is in het vinden van dingen die gelijk zijn en vervolgens, onbewust, daar bevestiging bij gaat zoeken. Je ziet dat er goede regels in staan, je "luie geest" zal aannemen dat ook de rest wel klopt. Maar als je dan één bug gevonden hebt, is het vaak dat je ook andere bugs ineens ziet (hoe kan het dat ik dit niet eerder gezien heb?).

Het ontslakken van je entities (decluttering)

We gaan de boel overzetten naar domain primitives. Deze bevatten zelf veel validaties en zorgen er daardoor voor dat het uit de entities weggehaald kan worden. We pakken even de punten uit hoofdstuk 4 erbij:

  • Oorsprong - komt de data van een legitieme bron?
  • Grootte - komt de grootte van de data overeen met wat we verwachten/wat algemeen gebruik is?
  • Lexicale inhoud - bevat het de juiste karakters en encoding?
  • Syntax - klopt het format?
  • Semantiek - is de data logisch, slaat het ergens op?

 

De laatste stap, de semantiek, is wat de entiteit zou moeten doen. De stappen hiervoor zouden al uitgevoerd moeten zijn voordat je de data toevoegt in een entiteit. We hadden al een class Quantity, er komt nu ook een class ISBN bij. Hierin zitten de validaties. De code is een stuk opgeschoond. Zit nu nog één controle op of de order al betaald is, maar daar komen we in hoofdstuk 7 op terug. Het is nu alleen nog maar controleren of het boek er is in de gewenste hoeveelheid en zo ja, voeg het toe. Het gebruik van domain primitives in entities levert deze voordelen op:

  • De invoer wordt altijd gevalideerd.
  • Validatie is consistent, dit wordt altijd door de constructor van de domain primitive gedaan.
  • De code van de entity wordt een stuk overzichtelijker, omdat allemaal bijkomende checks niet meer uitgevoerd hoeven worden.
  • Daardoor wordt de code leesbaarder en zie je de "intentie" van de actie.

 

Door domain primitives mee te geven als parameters aan functies zorg je ervoor dat entities goed-gevalideerde input ontvangen.

Wanneer gebruik je domain primitives in entiteiten?

De auteurs zien eigenlijk geen situatie waarin je er niet gebruik van zou maken. De extra "wrappers" zorgen voor een kleine afname van de performance, maar dat is minimaal. Gebruik het niet alleen bij nieuwe code, maar mocht je onderhoud moeten plegen en een mogelijkheid zien om bestaande code beter te maken door domain primitives te gebruiken, ga je gang. In hoofdstuk 12 gaat het boek dieper in op Legacy Code.

Taint analysis

In computer science wordt het detecteren van mogelijk gevaarlijke invoer en afdwingen dat het gevalideerd wordt voor het gebruikt wordt "taint analysis" genoemd. Ingevoerde tekst kan javascript bevatten wat een keylogger wil installeren, SQL commando's voor het legen van tabellen. Elke input wordt als verdacht beschouwd tot de input vrijgegeven is, wat door een bepaald mechanisme uitgevoerd wordt. Op het moment dat je een nog verdachte input gebruikt om data te tonen aan de gebruiker (alert('test')) of in de database toe te voegen, dan moet taint analysis een alarm activeren.

Het interessante van taint analysis is, is dat het tijdens runtime uitgevoerd kan worden. Elke input wat door het systeem gaat krijgt een "taint bit". Door dit af te vangen wordt nog niet gevalideerde output afgevangen. Elke tool kan zijn eigen acties uitvoeren, maar meestal volgen ze dezelfde terminologie. Het framework is beschreven in "Dytan: A Generic Dynamic Taint Analysis Framework" (2007) door James Clause, Wanchun Li, and Alessandro Orso of the Georgia Institute of Technology: link.

  • Taint sources: de plaatsen waar "smerige invoer" binnen kan komen (user interface, importbestanden, koppelingen met externe systemen)
  • Untaining: de manier waarop data gecontroleerd wordt en "vrijgegeven" kan worden
  • Propagation policy: wat bepaalt of de data als "tainted" gemarkeerd is of juist niet als de data vewerkt of gecombineerd is
  • Taint sinks: de plaatsen waar de data in een mogelijk gevaarlijke situatie kan komen: getoond op het scherm, weggeschreven naar de database of beide


Dit framework zit behoorlijk diep op systeemniveau. Het moet bepalen waar het wel zit (Inputstream.read) en waar niet (Random.nextBytes). Voor zover ik kan zien is dit redelijk experimenteel en niet iets wat we gaan gebruiken.