Secure by Design - De ultieme samenvatting

Ingediend door Dirk Hornstra op 28-jun-2021 22:45

Van januari 2020 tot mei 2021 hebben we elke maand een hoofdstuk van het boek "Secure by Design" behandeld. Je kunt dat teruglezen bij het laatste hoofdstuk (hoofdstuk 14): link, je ziet daar ook de links naar alle andere afzonderlijke hoofdstukken. Het is dan wel een beetje versnipperd en langdurig, als ik zelf een boek lees, lees ik deze meestal in 1x door. Daarom verzamelen we alle samenvattingen van de hoofdstukken in dit artikel, hiermee hebben we een totaaloverzicht en kun je hopelijk je voordeel doen met de inzichten van dit boek.

Hieronder de losse samenvattingen omgezet naar 1 verhaal. De korte versie luidt:

  • Maak van je objecten "domein primitieven". Geen strings en integers die heen en weer gegooid worden, maar objecten met hun eigen validatie(s).
  • Denk goed na of jouw variabelen wijzigbaar moeten/mogen zijn, of dat ze "readonly" gemaakt kunnen worden. Als dat zo is en je dwingt het af in code, dan voorkom je (opzettelijke) problemen.
  • Heb je variabelen die 1x gelezen mogen worden (en niet vaker)? Zoals wachtwoorden (om te voorkomen dat ze per ongeluk toch gelogd worden)? Maak er read-once objecten van.
  • Laat in je test-projecten niet alleen de "happy flow" testen, maar ook de uitzonderingen. Dat het goed gaat is leuk, maar je wilt juist dat als er wat fout gaat, dit ook (snel) duidelijk is.
  • Log de fouten. Zorg dat dit los staat van bijvoorbeeld audit-gegevens (wie heeft wat op welk moment aangepast)? Zorg dat er geen gevoelige gegevens gelogd worden.
  • Bouw je applicatie als een cloud-applicatie. Zorg dat er geen onderlinge afhankelijkheden zijn.
  • Gebruik het cicuit-breaker principe. Als een externe API eruit ligt, wil je dat jouw applicatie helemaal gaat vastlopen op openstaande externe requests? Nee, zorg dat de calls niet uitgevoerd worden, pas later mondjesmaat weer en als de API weer bereikbaar is, de normale code weer draait.
  • Valideer de invoer. Beschouw alle invoer als ongeldig, totdat jouw checks zeggen dat het wel correct is. Hiervoor zijn de 5 onderdelen:
  1. oorsprong: is de data afkomstig van een legitieme bron?
  2. grootte: is de grootte te verklaren?
  3. lexicale inhoud: bevat de inhoud de juiste tekens en encoding?
  4. syntax: klopt het format?
  5. semantiek: "slaat het ergens op"?
  • Zorg dat je bij blijft met de huidige bedreigingen op software gebied, bijvoorbeeld via OWASP. Als je weet wat de risico's zijn, weet je ook hoe je moet bouwen om dat te voorkomen.

 

De uitgebreide samenvatting:

  • 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.

 

  • 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.


"Mutable state", oftewel, wijzigbare objecten/waardes zijn voor een groot deel de basis van code. Waarom zou je naar een webshop gaan al je geen producten in je winkelwagen zou kunnen zetten (inhoud winkelwagen wijzigbaar), en deze niet kunnen afrekenen (afboeken voorraad, bijboeken inkooporder). Mutable state kan op meerdere manieren uitgewerkt worden. Een nieuwe entiteit die aangemaakt wordt moet zich aan jouw business-rules houden. Als dat niet gebeurt heb je de poppen aan het dansen, bugs en mogelijke veiligheidsproblemen die moeilijk op te sporen zijn kunnen jouw applicatie onderuit halen. In dit boek gaan we van start met domain driven design en het gebruik van entiteiten om dit probleem te lijf te gaan.

Als je variabelen hebt die eigenlijk maar één keer ingesteld worden en daarna nooit meer wijzigen hebt, zorg dat ze immutable zijn (readonly in C#).
Als je "entity state-objecten" hebt waarin acties (functies) zitten die niet altijd uitgevoerd mogen worden, maak een los object voor de "state" waarin een object zich kan bevinden, zodat je daar de controles uitvoert en niet in je entity-object duizenden "if-then" regels hebt.
Voor de read-only weergaves van gegevens kun je het beste gebruik maken van snapshots. Het zorgt ervoor dat je database alleen gelockt wordt op de plek waar het noodzakelijk is.
We zijn gewend een object te maken en die functies en eigenschappen te geven. Maar als je een heel groot systeem hebt met verschillende stadia, dan raadt het boek aan om per stadium een verschillend object aan te maken. Het object van type "baby" zou de functie maakDTPprikAfspraak(), planMelkDrinkActie() en laatBoertje() kunnen bevatten, waarbij het object van type "volwassene" de functie drinkBier(), bestelPatatjeShoarma() kan hebben. Hoewel beiden als één object "persoon" aangemaakt kunnen worden, zie je dat bepaalde functies alleen maar actief zijn in bepaalde stadia. Door een eigen entiteit per stadium te hebben kun je je richten op de zaken die voor dat stadium van belang zijn.

Op dit moment zorgt onze build-straat (Jenkins) dat als code niet gecompileerd kan worden, deze ook niet gedeployed wordt. Dit hoofdstuk zegt eigenlijk dat je testen aan je projecten moet toevoegen en deze ook in je build-straat zou moeten meenemen. Vaak zijn veel testen functioneel, maar er zouden ook meer security-gerelateerde testen gemaakt moeten worden. Test je code op geldige input, input die de grenswaarden raakt, ongeldige input en "extreem ongeldige input", gooi maar eens 20.000 tekens in een invoerveld en kijk wat er gebeurt. Als je features gebruikt (delen van code actief maken voor nieuwe functionaliteit) zorg dat je met testen kunt controleren dat code niet ongewenst op productie actief wordt. Zorg dat je test met load op je applicatie, zodat je weet tot hoeveel gebruikers deze nog acceptabel werkt. Controleer je configuratie (geautomatiseerd) en ook de default-instellingen van externe bibliotheken. Mooi voorbeeld hiervan is AutoMapper. Gebruiken we als ORM in onze applicaties, maar met verschillende updates is functionaliteit anders geworden of volledig weggevallen. Met een goed test-plan moeten we van tevoren kunnen controleren wat een update voor impact heeft op onze code.

Controleer of je excepties wel fouten zijn. Zijn het "verwachte failures", handel ze dan af met Result-objecten.
Let goed op wat je logt. Sla je een volledig Account-record met wachtwoord en salt op? Dat gaat je waarschijnlijk een data-lek opleveren.
Bouw je systemen zodat ze load kunnen verdragen. Zorg voor een circuit-breaker op plekken waar je afhankelijk bent van externe diensten. Stel time-outs in. Gebruikt Queue's. Gebruik "bulk-heads" om te zorgen dat bij problemen op locatie X in je code, je er op de andere plekken geen last van hebt.

Het punt van de externe diensten is een "goeien één". Stel dat Buckaroo er een halve dag uit zou liggen. Waarschijnlijk gaan alle onze check-outs "op hun plaat". Eigenlijk zou er "iets" moeten komen waardoor we 1. de melding tonen: er kan geen verbinding gemaakt worden met de betaalprovider. 2. de circuit-breaker zou hier ingezet moeten worden, waarom zou je kopers nog doorsturen als je al weet dat het niet gaat werken? 3. die circuit-breaker kan zo nu en dan wel mensen doorlaten om te checken of de dienst weer werkt. 4. het mooiste zou natuurlijk zijn dat je een soort dienst hebt, waarin je de bestelling tijdelijk opslaat, en de melding toont: op het moment dat de betaalprovider weer beschikbaar is, ontvang je van ons een e-mail met een link om je bestelling af te ronden. We hebben dit werkend bij een klant waarbij de volgende ochtend de mail verstuurd wordt: "er staan nog producten in je winkelwagen, klik hier om je order af te ronden".

Denk na over je software alsof het een cloud-oplossing is. Het hoeft nog geen cloud-oplossing te zijn, maar je moet het zo bouwen dat het bij wijze van spreken morgen in de cloud zou kunnen draaien en je dus zonder al teveel problemen extra server kunt bij pluggen om load op te vangen. De punten die benoemt worden kun je hierna wel lezen. Één van de punten die hier benoemd worden is dat in je log-gegevens geen gevoelige data geplaatst moet worden. Daar heeft Jeldert een tijd geleden een "goede actie" in doorgevoerd (wachtwoorden komen niet meer in het Elmah-log). Wat hier ook benoemd wordt is het "vasthouden van state". We zien (volgens mij) nog vaak de software als een groot geheel van functies, waarbij het eigenlijk losse onderdelen zouden moeten zijn, waardoor je de schaalbaarheid vergroot. In het orderproces wordt aan het einde een bericht naar het ERP gestuurd en de mail naar de klant afgehandeld. Op dat moment heb je de "ordergegevens" in je sessie o.i.d. Maar als dat weg zou vallen, kun je deze essentiële acties niet (meer) uitvoeren. En ik ben voorstander van de 3-R theorie: rotate, repave, repair. Rotate: je hebt ergens een secret. Maar eigenlijk zou je die 1x in de zoveel tijd moeten vervangen/bijwerken. Want als die maar lang genoeg gelijk blijft, dan komt er een moment dat iemand de juiste waarde raadt. Repave is het overschrijven van de data met je eigen data, zodat je zeker weet dat er geen "corrupte" data blijft staan. En repair, het zo snel mogelijk doorvoeren van patches (of updates op bijvoorbeeld nuget-packages) is natuurlijk hoe het zou moeten werken.

Conclusie, doe geen aannames, woorden die in het ene systeem X betekenen, kunnen in een andere systeem Y betekenen (een order bij BOL.com is iets anders dan een order die een soldaat krijgt) en zorg voor goed intern en extern overleg. En zoals ook in het boek getoond wordt, maak schema's om zaken visueel duidelijk te krijgen, het zegt soms meer dan een A4 vol met tekst.

Vaak werk je met "oude code", "legacy code", hoe je in code waarin niet nagedacht is over domein-objecten, goede validaties van input je de weg kunt vinden en je de code beter achterlaat dan je die gevonden hebt. Als je een functie (of constructor van een class) hebt met meerdere gelijksoortige parameters, zet deze om naar domeinobjecten zodat "per ongeluk" omwisselen van die parameters niet voor kan komen. Dat kun je in 1x doorvoeren, gefaseerd (zet in je constructor/functies de waardes om naar domeinobjecten zodat je dan de validatie uitvoert) of maak een extra functie aan met domein-objecten in de parameters, zodat je rustig code om kunt zetten.

Controleer wat je logt. Dat je applicatie crasht is niet fijn, maar dat je ook de log-database laat crashen omdat de input zo groot is dat andere applicaties moeten wachten op jouw log-actie, dan heb je nog meer problemen. Maar ook dat je niet goed afgeschermde credentials e.d. als plain-tekst naar een logdatabase wegschrijft.

Defensief coderen is goed, maar als in een class elke functie op null-waardes en andere controles moet uitvoeren (10% werkende code, 90% validatie van variabelen), dan gaat er iets niet goed. Zet die waardes om naar domein-objecten, laat die de validaties uitvoeren, zodat jouw "echte" code beknopt, leesbaar en te onderhouden blijft.

Don't Repeat Yourself is goed. Maar het samenvoegen van code die er "hetzelfde uit ziet" is niet altijd een goede actie. Soms gaat het om verschillende functionaliteit die wel degelijk gescheiden moet blijven. DRY gaat over de logica. Dus als er een postcode gecontroleerd wordt en er op 20 plaatsen met verschillende regexen en string-lengte dezelfde validatie uitgevoerd wordt, dat is "dubbele code" en dat moet je dus opschonen.

Het omzetten van variabelen naar domein-objecten is niet voldoende. De validaties, grenzen moeten uitgewerkt zijn. Test niet de "happy-flow-cases", maar ook met de absurde waardes. Hoewel een normale gebruiker die niet zal invoeren zijn er genoeg mensen online die het wel zullen proberen.
En bij het omzetten van een variabele naar een domein-object, controleer goed of je volledig bent. Het omzetten van een bedrag naar een "Amount/Aantal" domein-object mist de valuta-soort, wat kan zorgen voor foutieve tellingen en andere mogelijke problemen.

Microservices zijn booming. Door processen los te koppelen zorg je dat bepaalde acties onafhankelijk van elkaar kunnen draaien, bijgewerkt kunnen worden, ontworpen zijn voor downtime en hun eigen "bounded context" hebben (dus order kan in het ene deel een winkelwagen met inhoud zijn en in het andere deel een afgeronde en betaalde order).

Maak een goed ontwerp van je microservices om te voorkomen dat er een 1 log systeem niet omgebouwd wordt naar een log systeem verdeeld over 20 microservices.

Met microservices gaat er meer verkeer over de lijn. Kunnen meer gegevens gecombineerd worden. Hou dus goed in de gaten wat "vertrouwelijke gegevens" zijn en welke gegevens "vertrouwelijk" worden op het moment dat ze gecombineerd worden. Zorg dat de stromen traceerbaar zijn. Als ik met een console-applicatie HTTP-requests afstuur op een microservice, accepteert die dan de foutieve data omdat de structuur klopt, of omdat ook de bijbehorende hash/checksums correct zijn?

Als er data gelogd wordt, scheidt deze dan naar het type data. Is het algemene fout-informatie wat developers nodig hebben om de oorzaak te vinden en het probleem op te lossen? Of is het een deel audit-log, waarbij bepaalde acties van gebruikers in het systeem vastgelegd moeten worden (creditering van facturen), zodat bij fraude-onderzoek en andere meldingen dit te onderbouwen is?

Voer niet alleen code-reviews uit, maar ook code-security reviews. Zorg dat er pen-testen uitgevoerd worden, deel de resultaten en zorg voor een oplossing voor het complete probleem en niet alleen van deze bevindig. Zorg dat je up-to-date blijft met de laatste security issues. Als je het vermoeden hebt dat er wat aan de hand is, ga niet alleen kijken wat er aan de hand is, maar mobiliseer je collega's en andere afdelingen om ook te kijken of er wat gebeurt en zo ja, wat er aan gedaan kan worden. Beter 10x vals alarm, dan 1x geen alarm en dat al je klantgegevens op straat liggen.