Försvar på djupet: Del 4 Säkra API:er

This page is also available in: 🇺🇸 English
19 September 2023

De första tre artiklarna handlade om att designa och få tag på en access token. Vi har också etablerat mönstret för hur vi går från identitet och scopes till rättigheter som vi baserar all fortsatt behörighetskontroll på.

I denna artikel diskuterar vi vad du behöver göra i implementationen av ditt API för att skydda dina funktioner och ditt data.

Ett starkt försvar på djupet enligt principen “Least privilege” betyder att vi behöver begränsa rättigheterna för varje anrop till ett absolut minimum. Vi implementerar en stark och finmaskig behörighetskontroll i sex steg:

  1. Validera att anropet är korrekt
  2. Validera att access token är korrekt
  3. Transformera access token till rättighetsmodell
  4. Validera att data i anropet är korrekt
  5. Validera behörighet att utföra operationen
  6. Validera behörighet till det data som efterfrågas eller påverkas

Vi går från en korrekt, verifierad access token (t ex en JWT) till ett objekt som representerar våra rättigheter i systemet. Med hjälp av denna rättighetsmodell implementerar vi en stark finmaskig behörighetskontroll i steg 4, 5 och 6.

Notera att vi i denna modell utelämnat det som bör finnas framför ditt API i form av infrastrukturskydd, t ex en brandvägg, WAF, API-gateway eller liknande. Exempelvis kan en WAF göra en första indatavalidering efter kända injection-attacker och vi kanske gör en grundläggande behörighetskontroll baserat på IP eller trusted device.

Det ena utesluter inte det andra, utan kompletterar varandra. Det är viktigt att vår API säkerhet inte enbart förlitar sig på tidigare skyddslager. Vi bygger försvar på djupet där vårt API tål publik exponering, enligt principen för Zero Trust.

Vi kan föra liknande resonemang för steg 4 och 5, där ordningen mellan dem kan variera beroende på ramverk etc. Exempelvis är det vanligt att man tidigt gör en grundläggande indata validering och sedan en djupare validering i sin domänlogik. Det viktiga är att det görs och att man avbryter felaktiga anrop tidigt, utan att konsumera onödiga systemresurser.

Det vi vill lyfta fram med modellen är att dessa sex steg behövs för en stark och finmaskig behörighetskontroll, samt att de implementeras med ett tvingade mönster så att kärnan, din domänlogik, aldrig exponeras utan behörighetskontroll.

Kasper Karlsson, Omegapoint

När jag utför penetrationstester är brister i dessa steg något av det första som jag provar. Det är mycket vanligt att det ger utdelning. Det är dessutom en vektor där jag kan extrahera data genom att gå direkt på tjänsten istället för att tex gå omvägen via någon annan användares konto.

Validera att anrop är korrekt (steg 1)

Att validera själva HTTP-anropet kanske man inte tänker på som utvecklare, men det är en viktig aspekt av ett säkert API. Om vi väljer en bra webb-server, med säkra grundinställningar, får vi detta gratis. Exempel på verifiering är att anropet är i korrekt format och har en rimlig storlek. En del produkter kan göra en djupare analys och avvisa ett anrop som t ex innehåller data som kan anses skadligt.

Validera att access token är korrekt (steg 2)

Verifiering av den access token som kommer med i anropet bör ske med hjälp av det ramverk som vi bygger vårt API på. Du bör endast konfigurera här, inte implementera denna kontroll själv. Med JWT som exempel behöver ramverket verifiera:

  1. Korrekt kryptografisk signatur
  2. Signerad av rätt IdP (oftast konfigurerad som en URL till vår IdP)
  3. Utställd med en audience som gäller för vårt API
  4. Korrekt typ, dvs. en access token och inte någonting annat
  5. Giltig, t ex med avseende på tid

Hur en JWT valideras definieras av https://tools.ietf.org/html/rfc8725 Det finns även andra typer av tokens än JWT. Om det t ex är en reference token, behöver referensen översättas till en access token först, genom uppslag mot IdP (kallas för “token introspection”).

För system med högre säkerhetsnivå används ofta access tokens som bundits till klienten, vanligt är att detta sker genom mTLS. Om din access token är certifikatsbunden skall bindningen till klientens certifikat valideras.

För att kunna verifiera en korrekt signatur behöver vi nyckelmaterial för vår IdP. Hur vi får tag i detta kan skilja sig åt. Det är vanligt att göra ett uppslag mot IdP med hjälp av “JSON Web Key Set” (JWKs). Det är ett protokoll för att hämta den publika delen av vår IdPs signeringsnyckel. Man kan också välja att installera nyckelmaterialet på samma maskin som vårt API kör på.

En viktig aspekt är att IdP kan rotera sitt nyckelmaterial. De allra flesta IdP-produkter stödjer JWKs och i praktiken tycker vi att en bra lösning är att vårt API startar om dagligen och gör ett uppslag vid uppstart.

Normalt ska ditt ramverk returnera en 401 Unauthorized om anropen görs med en ogiltig access token.

Transformera access token till rättighetsmodell (steg 3)

Efter att vi har validerat vår access token kan vi transformera informationen den innehåller till ett objekt av vår rättighetsmodell. Objektet innehåller all den information (rättigheter) som vi behöver i vår domänlogik för att reda ut om användaren har rätt till den funktion och det data som efterfrågas. Från och med denna punkt arbetar vi enbart med vårt rättighetsobjekt, inte med information från vår access token.

Se artikel 2, Claimsbaserad behörighetskontroll för detaljer kring hur du kan implementera en transformation.

Validera att data i anropet är korrekt (steg 4)

I detta steg validerar vi det data som användaren har skickat i anropet utifrån vår domänmodell. Det kan t ex vara att telefonnummer ska ha ett korrekt format. Om denna validering fallerar så returnerar vi alltid en 400 Bad Request.

Denna tidiga validering ger oss en värdefull möjlighet att avvisa och logga felaktiga anrop. Ett säkert system genererar normalt inte några brott mot denna typ av validering av indata. Ett automatiserat larm baserat på loggning av felaktiga anrop kan fånga många angreppsförsök och göra dig uppmärksam på att någon försöker bryta sig in i ditt system.

Om du implementerar ditt API i ett starkt typat språk, är det ett kraftfullt mönster att välja en typ som minimerar problematiken med injektion. Istället för att deklarera parametrar i ditt API som strängar, kan du kanske använda heltal, boolean osv. Det reducerar möjligheten för en attackerare att skicka in värden i syfte att bryta sig ur den avsedda funktionen och förändra betydelsen av anropet.

Notera att validering av indata inte är ett komplett skydd mot injektionsattacker. Det finns många exempel på situationer där vi behöver ge användaren möjlighet att bifoga data utan restriktioner. I slutändan behöver data korrekt utdatakodning för den situation där det ska användas.

Till exempel ett anrop till /api/products/1, där angriparen istället för värdet 1 försöker skicka en sträng som innehåller en injektionsattack. Om deklarationen i vårt API av parametern för produkt id är av typen heltal, så sköter ramverket valideringen åt oss. Om produkt id istället är av typen sträng behöver vi själva verifiera att värdet t ex enbart består av siffror.

Indatavalidering är ett lager i vårt försvar på djupet. Ett starkt mönster för att minska attackvektorer är att så tidigt som möjligt gå över till domänprimitiv, så att din domänlogik endast jobbar med domänspecifika typer.

Bra exempel på denna djupa indatavalidering är kund-id och antal. Kund-id är ingen obegränsad sträng som kan innehålla vilka tecken som helst. I din domän kanske kund-id består av tre stora bokstäver och tre siffror. På samma sätt är antal inget obegränsat heltal, utan ett tal från 1 till 100. Det omöjliggör att lägga en order på t ex minus ett eller flera tusen.

Detta mönster är hämtat från boken Secure By Design och begränsar risken för injektionsattacker men även överbelastning eller fel till följd av korrupt data. Säkerhet är också kvalitet och tillgänglighet.

https://www.manning.com/books/secure-by-design

Validera behörighet att utföra operationen (steg 5)

I detta steg validerar vi om användaren har rätt att utföra operationen utan att hämta data eller utnyttja andra resurser som skapar last i systemet. Om dessa krav på rättigheter inte är uppfyllda, returnerar vi anropet direkt med en 403 Forbidden.

Till exempel ett anrop till /api/products/1, som kräver rättigheten att läsa produkter och att användaren är med i rollen “users”. Vi kan tidigt returnera en 403 Forbidden om något av detta inte stämmer, utan att behöva göra någon slagning i databasen för produkter.

Validera behörighet till data (steg 6)

I det sista steget har vi normalt sett gjort en slagning efter det data som anropet gäller, antingen för att det ska returneras, eller för att det ska ändras eller tas bort. Till exempel kan vi i detta läge verifiera att det data som ska returneras hör till användarens organisation eller roll.

Ibland är det enkelt att avgöra om användaren har rätt till det data som efterfrågas. I andra fall krävs komplex domänlogik. Ett exempel är sökfunktioner där vi kan behöva göra behörighetskontrollen sent, efter att läsning av data gjorts.

Om denna verifiering fallerar har vi vanligtvis två returkoder vi kan välja mellan. 404 Not Found är lämplig om vi inte vill avslöja att användaren till exempel har begärt data som tillhör en annan organisation. 403 Forbidden kan vara lämplig om vi vill uppmärksamma klienten på att data existerar, men inte är tillgänglig på grund av hennes rättighetsmodell.

Själva uppslaget av data i till exempel en databas bör ske med ett databaskonto som ger en så begränsad tillgång till data som möjligt. Om ett API har behov av att läsa delar av allt data som finns i en databas, bör den ges att konto som är begränsat till det syftet. ”Least privilege” gäller såväl användarkonton som systemkonton.

Till exempel ett anrop till /api/products/1 där användaren inte har rätt till just den produkten. Vi kan välja att i det läget returnera en 404 Not Found.

Notera också att tillgång till data kan gälla även om det är ett kommando som ska utföras, och inget data returneras. Ett enkelt exempel är HTTP DELETE till /api/products/1 där vi också behöver verifiera att användaren har rätt till just den produkten.

Vår erfarenhet är att det är ett mycket vanligt fel att inte verifiera att användaren har rätt till data som returneras eller avses vid ett kommando.

Martin Altenstedt, Omegapoint

Jag ser ofta att GUID används som id och att det argumenteras för att det är ett tillräckligt skydd eftersom det är svårt att gissa en GUID. Det kan stämma, men en GUID är sällan kryptografiskt säker och ofta är värdet en direkt refererens till objektet. Om man delar ut det värdet kan det inte revokeras, och tillgång till objektet inte längre kontrolleras.

Loggning och hantering av fel

Angripare utnyttjar ofta oväntade beteende i systemet. Vanligt är att dessa sårbarheter beror på brister i felhantering som gör att systemet kan försättas i ett odefinierat tillstånd. För ett säkert API är det viktigt att vi har full kontroll över hur applikationen fungerar, en del av detta är ett tydligt och konsekvent mönster för felhantering och loggning.

Central loggning av fel i systemet, i kombination med användning av rätt returstatus är en grundförutsättning för att kunna detektera och agera på intrångsförsök. Ett system som i normal drift inte skapar fel och returnerar HTTP-statuskoder i 200-serien ger oss förutsättningar att konfigurera automatiserade larm för oväntade händelser.

För ett system som består av flera API så underlättar det om loggningen dessutom är centraliserad och korrelerad. Det betyder att det finns ett centralt ställe där vi kan se loggarna för hela systemet, och dels kan följa tråden av anrop när ett API gör anrop till ett annat, i en kedja.

Idag finns det många bra produkter på marknaden för centraliserad loggning. En viktig funktion som några av dem erbjuder är anomalidetektering, dvs. automatiserad alarmering om vi har avvikelser från det normala mönstret i hur vårt system fungerar. Detta kan vara svårt att bygga på egen hand och ger bra förutsättningar att detektera intrångsförsök.

Tobias Ahnoff, Omegapoint

Även om vi alltid bör undvika att logga känsligt data, som t ex personnummer eller access tokens, så utgör loggar ofta känsliga datakällor och därför behöver dessa skyddas på samma sätt som affärsdatat. Det är mycket stor risk att t ex persondata hamnar i loggar på ett eller annat sätt.

Om du genomför ett oberoende penetrationstest på ditt system, passa på att samtidigt verifiera att din lösning för loggning detekterar intrångsförsöken!

Kasper Karlsson, Omegapoint

Vid penetrationstester är det vanligt att applikationer läcker känsliga uppgifter vid fel, t ex stack-trace eller t o m connection-strängar.

Martin Altenstedt, Omegapoint

Tänk på att logga i rätt syfte och på rätt nivå. Vi riskerar annars att missa viktiga loggar. Det du som utvecklare behöver under utveckling är kanske inte samma sak som behövs för driftövervakning.

Summering

Artikelserien har så här långt diskuterat hur du designar ditt system för att implementera en stark behörighetskontroll. Vi har också lyft fram vikten av indatavalidering och centraliserad loggning.

I nästa artikel kommer vi titta på infrastruktur, exempelvis transportlagerskydd och lagring av data.

En fördjupning av denna artikel, med exempel implementation, finns i den kompletterande artikeln Secure APIs by design.

Se Defence in Depth för ytterligare material och kodexempel kopplade till den här artikelserien.


Fler artiklar i serien: