Secure by Design - Hoofdstuk 12

Ingediend door Dirk Hornstra op 30-mar-2021 09:05

In januari 2020 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), in juni met hoofdstuk 5 (link), in juli met hoofdstuk 6 (link), in augustus hoofdstuk 7 (link), in september hoofdstuk 8 (link), in november hoofdstuk 9 (link), in februari 2021 hoofdstuk 10 (link), in maart 2021 hoofdstuk 11 (link) en nu wordt het tijd voor hoofdstuk 12.

De Timo-samenvatting

Dit hoofdstuk gaat over "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.

De titel van hoofdstuk 11 is "Guidance in legacy code".

We zijn hiermee in het laatste deel van het boek gekomen, waarin we de "fundamentals" gaan toepassen. Bij legacy code denk je al gauw aan oude programmeertalen, Cobol, Fortran. Maar dat hoeft niet zo te zijn, het kan ook code van jezelf zijn die inmiddels een jaar oud is. Je bent zelf een jaar "gegroeid" en het is best mogelijk dat als je jouw eigen code bekijkt met je hoofd schudt en denkt "dat zou ik nu anders doen".

In dit hoofdstuk gaat het om "oude code" waarbij niet in het achterhoofd met security-issues rekening gehouden is. Dus we gaan mogelijke problemen spotten en die proberen te fixen. We gaan beginnen met het onderzoeken waarom "onduidelijke parameters" in constructors en functies een veel voorkomende bron van veiligheidsrisico's zijn. Daarna krijgen we tips waar we op moeten letten bij loggen en waarom defensieve code-constructies problematisch kunnen zijn. Dan komt er een voorbeeld waarbij de code er netjes uitziet, maar er toch een probleem blijkt te zijn. Aan het einde worden domain primitives behandeld, bij introductie in legacy code kan dat voor problemen zorgen.

Er volgt een korte toelichting op de domain primitives die in het einde van het hoofdstuk nog terug komen. Zo gaat er in bestaande code veel integers, strings en andere data door de applicatie. Veel developers die de systemen gaan "opschonen" beginnen met wrappers te maken die deze zaken omhullen om er een expliciet type van te maken. Maar daardoor maak je het systeem gecompliceerder en ben je eigenlijk niet bezig wat je met een domain-primitive zou moeten doen. We zien het voorbeeld van een reservering-functie, deze heeft een read-only string wat het ordernummer is, dan moet elke class of functie wat iets met reservering en ordernummer doet hetzelfde principe gebruiken, dus niet dat ergens een ordernummer een long is: een numerieke waarde, want dan klopt het model niet meer.

Dubbelzinnige parameterlijsten (ambigious parameter lists)

We zien de functie verzenditems die een aantal parameters heeft: int itemId, int quantity, Address billingAddress, Address invoiceAddress. Omdat parameters van hetzelfde type zijn, bestaat het risico dat je ze per ongeluk omwisselt. Dus gaat de pallet met goederen naar het postbus-adres. Worden er niet 5 producten met ID 250 verstuurd, maar 250 producten met ID 5. Dit omwisselen kwam (als het goed is) in hoofdstuk 5 voorbij: link. Sommige developers vragen of het Builder pattern uit hoofdstuk 6 (link) hier een oplossing voor kan zijn, maar volgens de schrijver is dit een plakbandje en kun je dan nog steeds parameters omwisselen.

Directe aanpak

De directe aanpak heeft als voorbeeld dat het parameters meteen aanpakt, het werkt het beste met een niet al te grote applicatie en een paar developers en als de hoeveelheid code niet heel groot is, is het snel te doen. De nadelen zijn dat in grote softwareprojecten er heel veel refactoring moet plaatsvinden. Als data-kwaliteit een "big issue" is, daar is het niet echt geschikt voor. Het kan dan ook het beste door 1 developer uitgevoerd worden.

We zien het voorbeeld van verzendItems, waarbij elke parameter nu een type object is. Het is een kwestie van vervangen en vervolgens de fouten in de gaten houden, want wat eerst er doorheen glipte, wordt nu wel geweigerd. En daar moet wel wat mee gedaan worden.

Discovery aanpak

De "discovery approach", het vinden en oplossen van de problemen voor je de API aanpast heeft als voordeel dat het goed werkt als de kwaliteit van data slecht is, het werkt met grotere code-projecten en meerdere teams. Nadelen zijn dat het lang duurt om door te voeren en als er veel wijzigingen doorgevoerd worden, is het maar de vraag of je dat bij kunt houden.

We zien in de code weer het voorbeeld van verzenditems. De parameters blijven gelijk, maar in de functie worden deze omgezet naar domein-items. Daar kan vervolgens een fout in opgeworpen worden (het aantal bestelde producten is 500, terwijl je maar maximaal 10 kunt bestellen: hier moet iets omgewisseld zijn). Zo kun je fouten gaan monitoren en de bron van de fouten opsporen en het daar herstellen. Als je uiteindelijk vertrouwen hebt in de code kun je over naar de directe aanpak en de parameters aanpassen.

Een nieuwe API

De "new API approach", waarmee je een nieuwe API maakt en langzaam maar zeker de oude API laat verdwijnen heeft als voordelen dat het incrementele refactoring ondersteunt, de grootte van de codebase niet uitmaakt en het ook met grote teams werkt. Bij de nadelen staat genoemd dat als data-kwaliteit een issue is, je het met de discovery-approach moet combineren.

Bij deze aanpak laat je de functie verzenditems intact. Maar je maakt ook een functie verzenditems die de domein-objecten gebruikt. Die roept vervolgens de originele (deprecated) functie aan. Op deze wijze kun je zelf delen van de codebase ombouwen naar de nieuwe functie en hou je de oude functie (en aanroepende code) werkend.

Loggen (van niet gecontroleerde strings)

Als je data logt (wat vaak tekst is), dan moet je eerst controleren wat je logt. Doe je dat niet, dan loop je het risico op het lekken van data en injection van data die je niet wilt. We zien het voorbeeld van een ordernummer wat gelogd wordt (het is geen domain-primitive, het is een string), dus in theorie zou het een string van 100 miljoen karakters of een stuk script zijn. En we zien het voorbeeld van het lekken van data. Er wordt een record opgevraagd en dat wordt gelogd, dus impliciet wordt het naar een string omgezet. Dat kan fout gaan, je ziet niet de inhoud, maar het type object. Of het datamodel breidt zich uit, bij een reservering wordt ook een tijdelijk user-account met wachtwoord vastgelegd: ook dat komt dan in je log-database.

Defensieve code-constructies

Controles op null-objecten e.d. zijn op zich prima. Maar als je code vol zit met die controles, functie A niet meer vertrouwt wat functie B aanlevert, dan gaat het niet helemaal lekker in je code.

Code die zichzelf niet vertrouwt

We zien het voorbeeld van een winkelwagen die interne functie aanroept. Er zitten null en validatie-checks op een ISBN nummer. Maar de ene functie krijgt het binnen van de andere functie, dus dat zou al goed moeten zijn. En als dat ISBN-nummer een domain-primitive was, zou al zien dat die controles niet nodig zijn.

Contracten en domain-primitives

Dit komt uit hoofdstuk 5, we zien hier het voorbeeld hoe de code aangepast wordt naar een Quantity-objecttype en een ISBN-objecttype. Het maakt je code een stuk overzichtelijker en duidelijk wat nu waar gedaan wordt.

Het gebruik van optionele parameters

In het boek wordt het voorbeeld van optionele parameters genoemd in JAVA. Maar in C# kun je ook bepaalde parameters optioneel maken. Kom je echter op het punt dat die parameter niet meer optioneel is, maar nodig om bijvoorbeeld een order af te ronden, dan zit je in de penarie.

Het foutief toepassen van DRY - niet het idee, maar focus op tekst

DRY (don't repeat yourself) heeft als achterliggende gedachte dat als we onze kennis in code vastleggen, dat op een centrale plaats gedaan moet worden. Het boek zegt dat veel programmeurs dit opvatten als "zorg dat code niet gedupliceerd wordt" en haal het naar 1 centrale plek, developers zoeken naar code die gelijk aan elkaar lijkt te zijn en voegt dit samen tot 1 functie. Het zoeken naar duplicaten kan echter "false positives" en "false negatives" opleveren.

De voorbeelden die we krijgen zijn Engelse postcodes, deze bevatten alleen nummers.

We zien het voorbeeld van 2 regex-acties in 1 stuk code. Identieke regex, bij de ene match is het op het ordernummer, bij het andere op de postcode. Iemand zou kunnen denken, dit is hetzelfde, dus gaan we samenvoegen naar 1 functie. Je ziet zoiets dan gaan naar een statische functie Util.MatchNumber(...). Je moet hier een Postcode-domain-object aanmaken en een OrderNumber-domain-object die elk hun eigen regex kunnen bevatten. Dit is het voorbeeld van een false positive.

Vervolgens zien we een voorbeeld waarbij een postcode wordt gecontroleerd met een regex, en op basis van lengte en of deze naar een integer omgezet kan worden. Hoewel het 2 verschillende functies lijken te zijn, doen ze wel precies hetzelfde. Dus als iemand deze niet gaat opschonen, dan heb je een false negative.

Onvolledige validatie in domein-types

Een applicatie kan er qua code netjes uitzien. Allemaal verschillende domein-types. Maar als er niet gevalideerd wordt of onvolledig, dan zijn er nog steeds veiligheidsrisico's (reeds besproken in hoofdstuk 4, 5, 6 en 7). We zien het voorbeeld van een "Quantity"-domein-type, waarbij alleen gecontroleerd wordt op niet null en waarbij deze "immutable" is. Maar in het echt kan deze waarde alleen tussen 1 en 500 variƫren. Daar moet in de constructor dus meteen op gevalideerd worden.

Test tot in het extreme

Als er al tests gemaakt worden, dan worden vaak normale situaties getest en een aantal randgevallen en enkele uitzonderingen om te controleren "of het werkt zoals gewenst". Maar je moet ook de uiterste gevallen testen. We zien het voorbeeld waarbij een reservering opgehaald wordt en dat wordt met een ordernummer gedaan: een string. Een ordernummer zal een nummer zijn, of iets met letters en cijfers, maar niet een tekst van 100-en tekens. Maar dat is op deze manier wel mogelijk. Ook als je er een domein-object van maakt, is het nog steeds afhankelijk van de controles die je daar uitvoert, dus ook in die gevallen moet je de extreme gevallen testen. In het boek wordt een MongoDB als voorbeeld gegeven, maar ook als we met Microsoft SQL Server een time-out krijgen, dan wil je natuurlijk niet dat in de output getoond wordt met welke database-server verbinding wordt gemaakt.

Partial domain primitives

Je domein-objecten moet je zo klein mogelijk houden. Maar als er niet voldoende informatie opgeslagen wordt, is het mogelijk dat het in de verkeerde context gebruikt wordt. We gaan hier een voorbeeld met geld bekijken. In het "extra info blok" staat dat je voor het opslaan van geld/bedragen in je database je geen double moet gebruiken omdat "je 1/100 niet exact kan weergeven met x tot de macht 2".  Uiteindelijk zul je daar afrondingsproblemen mee krijgen. Het boek is op Java gericht en geeft de BigDecimal als voorbeeld, deze is ontworpen om exacte decimalen op te slaan. Of je hebt het alternatief dat je in 2 kolommen de data als integers opslaat: het aantal dollars/euro's en het aantal centen. Of je slaat het op als een long-waarde, waarbij je de waarde in centen opslaat, 1 euro is dus 100 eurocent.

Het boek geeft het voorbeeld waarbij bedragen worden opgeslagen als een domein-object, maar ze zijn gekoppeld aan een land, dus het is Amerikaanse dollar, Euro en Sloveense Tolars. Een developer die alle verkopen moet optellen, telt alle bedragen bij elkaar op en zegt: dit is de omzet. Het antwoord is fout omdat er verschillende soorten valuta (met afwijkende koersen) bij elkaar opgeteld zijn. En om het erger te maken gaan de Slovenen over naar de euro, dus op een bepaald moment zijn hun verkopen euro's geworden en klopt het principe niet meer dat 1 land 1 valuta heeft. Om de prijs als een "amount" domein-object weer te geven, als een soort wrapper om een getal is in dit geval te kort door de bocht. Het boek komt met het domein-object Money waarin je aangeeft wat de "amount" is, maar ook de "currency".