SQL-injektion og hvordan man forhindrer det?

Udholdenhedstop

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN

1. Introduktion

På trods af at det er en af ​​de mest kendte sårbarheder, fortsætter SQL Injection med at placere sig på toppen af ​​den berygtede OWASP Top 10-liste - nu en del af den mere generelle Indsprøjtning klasse.

I denne vejledning udforsker vi almindelige kodefejl i Java, der fører til et sårbart program, og hvordan man undgår dem ved hjælp af de API'er, der er tilgængelige i JVM's standard runtime-bibliotek. Vi dækker også, hvilken beskyttelse vi kan få ud af ORM'er som JPA, Hibernate og andre, og hvilke blinde pletter vi stadig skal bekymre os om.

2. Hvordan applikationer bliver sårbare over for SQL-injektion?

Injektionsangreb fungerer, fordi den eneste måde at udføre en given beregning for mange applikationer er at generere dynamisk kode, der igen drives af et andet system eller komponent. Hvis vi i løbet af genereringen af ​​denne kode bruger upålidelige data uden korrekt sanering, efterlader vi en åben dør for hackere at udnytte.

Denne erklæring lyder måske lidt abstrakt, så lad os se på, hvordan dette sker i praksis med et lærebogeksempel:

public List unsafeFindAccountsByCustomerId (String customerId) kaster SQLException {// UNSAFE !!! Gør ikke dette !!! String sql = "select" + "customer_id, acc_number, branch_id, balance" + "from Accounts where customer_id = '" + customerId + "'"; Forbindelse c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

Problemet med denne kode er indlysende: vi har sat det Kunde ID'S værdi i forespørgslen uden nogen validering overhovedet. Intet dårligt vil ske, hvis vi er sikre på, at denne værdi kun kommer fra pålidelige kilder, men kan vi?

Lad os forestille os, at denne funktion bruges i en REST API-implementering til en konto ressource. At udnytte denne kode er trivielt: alt hvad vi skal gøre er at sende en værdi, der, når den sammenkædes med den faste del af forespørgslen, ændrer dens tilsigtede adfærd:

krølle -X FÅ \ '// localhost: 8080 / konti? customerId = abc% 27% 20eller% 20% 271% 27 =% 271' \

Forudsat at Kunde ID parameterværdi bliver ikke markeret, indtil den når vores funktion, her er hvad vi ville modtage:

abc 'eller' 1 '=' 1

Når vi forbinder denne værdi med den faste del, får vi den endelige SQL-sætning, der udføres:

vælg customer_id, acc_number, branch_id, balance fra konti, hvor customerId = 'abc' eller '1' = '1'

Sandsynligvis ikke hvad vi har ønsket ...

En smart udvikler (er vi ikke alle sammen) tænker nu: ”Det er fjollet! Jeg ville aldrig brug strengkombination til at oprette en forespørgsel som denne ”.

Ikke så hurtigt ... Dette kanoniske eksempel er faktisk fjollet men der er situationer, hvor vi måske stadig har brug for det:

  • Komplekse forespørgsler med dynamiske søgekriterier: tilføjelse af UNION-klausuler afhængigt af brugerleverede kriterier
  • Dynamisk gruppering eller ordning: REST API'er, der bruges som backend til en GUI-datatabel

2.1. Jeg bruger JPA. Jeg er sikker, ikke?

Dette er en almindelig misforståelse. JPA og andre ORM'er fritager os fra at oprette håndkodede SQL-sætninger, men de forhindrer os ikke i at skrive sårbar kode.

Lad os se, hvordan JPA-versionen af ​​det foregående eksempel ser ud:

public List unsafeJpaFindAccountsByCustomerId (String customerId) {String jql = "from Account where customerId = '" + customerId + "'"; TypedQuery q = em.createQuery (jql, Account.class); returner q.getResultList () .stream () .map (dette :: toAccountDTO) .collect (Collectors.toList ()); } 

Det samme spørgsmål, som vi tidligere har påpeget, er også til stede her: vi bruger uvalideret input til at oprette en JPA-forespørgsel, så vi udsættes for den samme slags udnyttelse her.

3. Forebyggelsesteknikker

Nu hvor vi ved, hvad en SQL-injektion er, lad os se, hvordan vi kan beskytte vores kode mod denne form for angreb. Her fokuserer vi på et par meget effektive teknikker, der er tilgængelige på Java og andre JVM-sprog, men lignende koncepter er tilgængelige for andre miljøer, såsom PHP, .Net, Ruby og så videre.

For dem, der leder efter en komplet liste over tilgængelige teknikker, herunder databasespecifikke, opretholder OWASP-projektet et SQL Injection Prevention Cheat Sheet, som er et godt sted at lære mere om emnet.

3.1. Parameteriserede forespørgsler

Denne teknik består i at bruge forberedte udsagn med spørgsmålstegnepladsholderen (“?”) I vores forespørgsler, når vi har brug for at indsætte en brugerleveret værdi. Dette er meget effektivt, og medmindre der er en fejl i JDBC-driverens implementering, immun over for udnyttelser.

Lad os omskrive vores eksempelfunktion for at bruge denne teknik:

public List safeFindAccountsByCustomerId (String customerId) kaster Undtagelse {String sql = "select" + "customer_id, acc_number, branch_id, balance from Accounts" + "where customer_id =?"; Forbindelse c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); ResultSet rs = p.executeQuery (sql)); // udeladt - behandle rækker og returnere en kontoliste}

Her har vi brugt prepareStatement () metode tilgængelig i Forbindelse eksempel for at få en PreparedStatement. Denne grænseflade udvider det almindelige Udmelding interface med flere metoder, der giver os mulighed for sikkert at indsætte brugerleverede værdier i en forespørgsel, før vi udfører den.

For JPA har vi en lignende funktion:

Streng jql = "fra konto hvor customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Udfør forespørgsel og returner kortlagte resultater (udeladt)

Når vi kører denne kode under Spring Boot, kan vi indstille ejendommen logging.level.sql til DEBUG og se, hvilken forespørgsel der faktisk er bygget for at udføre denne operation:

// Bemærk: Output formateret til at passe til skærmbillede [DEBUG] [SQL] vælg konto0_.id som id1_0_, konto0_.acc_number som acc_numb2_0_, konto0_.balance som balance3_0_, konto0_.branch_id som branch_i4_0_, konto0_.kunde_id som kunde5_0_ fra konto0 .customer_id =?

Som forventet opretter ORM-laget et forberedt udsagn ved hjælp af en pladsholder til Kunde ID parameter. Dette er det samme, som vi har gjort i den almindelige JDBC-sag - men med et par udsagn mindre, hvilket er rart.

Som en bonus resulterer denne tilgang normalt i en bedre forespørgsel, da de fleste databaser kan cache forespørgselsplanen, der er knyttet til en forberedt erklæring.

Bemærk venligst at denne tilgang kun fungerer for pladsholdere, der bruges somværdier. For eksempel kan vi ikke bruge pladsholdere til dynamisk at ændre navnet på en tabel:

// Dette VIRKER IKKE !!! PreparedStatement p = c.prepareStatement ("vælg antal (*) fra?"); p.setString (1, tabelnavn);

Her hjælper JPA heller ikke:

// Dette VIRKER IKKE !!! String jql = "select count (*) from: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tabelnavn", tabelnavn); returner q.getSingleResult (); 

I begge tilfælde får vi en runtime-fejl.

Hovedårsagen bag dette er selve karakteren af ​​en forberedt erklæring: databaseservere bruger dem til at cache den forespørgselsplan, der kræves for at trække resultatsættet, hvilket normalt er det samme for enhver mulig værdi. Dette gælder ikke for tabelnavne og andre konstruktioner, der er tilgængelige på SQL-sproget, f.eks. Kolonner, der bruges i en bestil efter klausul.

3.2. JPA Criteria API

Da eksplicit JQL-forespørgsel er hovedkilden til SQL-injektioner, bør vi foretrække brugen af ​​JPA's Query API, når det er muligt.

For en hurtig primer på denne API henvises til artiklen om Hibernate Criteria-forespørgsler. Det er også værd at læse vores artikel om JPA Metamodel, som viser, hvordan man genererer metamodelklasser, der hjælper os med at slippe af med strengkonstanter, der bruges til kolonnenavne - og de runtime-bugs, der opstår, når de ændres.

Lad os omskrive vores JPA-forespørgselsmetode for at bruge Criteria API:

CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Root root = cq.from (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)); TypedQuery q = em.createQuery (cq); // Udfør forespørgsel og returner kortlagte resultater (udeladt)

Her har vi brugt flere kodelinjer for at få det samme resultat, men opadrettede er det nu vi behøver ikke bekymre os om JQL-syntaks.

Et andet vigtigt punkt: på trods af dets bredde Criteria API gør oprettelsen af ​​komplekse forespørgselstjenester mere ligetil og mere sikker. For et komplet eksempel, der viser, hvordan du gør det i praksis, skal du se på den tilgang, der anvendes af JHipster-genererede applikationer.

3.3. Sanering af brugerdata

Data Sanitization er en teknik til at anvende et filter på brugerleverede data, så det kan bruges sikkert af andre dele af vores applikation. Implementeringen af ​​et filter kan variere meget, men vi kan generelt klassificere dem i to typer: hvidlister og sortlister.

Sorte lister, som består af filtre, der forsøger at identificere et ugyldigt mønster, er normalt af ringe værdi i forbindelse med SQL Injection-forebyggelse - men ikke til påvisning! Mere om dette senere.

Hvidlisterfungerer derimod særligt godt, når vi kan definere nøjagtigt, hvad der er et gyldigt input.

Lad os forbedre vores safeFindAccountsByCustomerId metode, så nu kan den, der ringer, også specificere den kolonne, der bruges til at sortere resultatsættet. Da vi kender sættet med mulige kolonner, kan vi implementere en hvidliste ved hjælp af et simpelt sæt og bruge det til at rense den modtagne parameter:

privat statisk endelig Sæt VALID_COLUMNS_FOR_ORDER_BY = Collections.unmodifiableSet (Stream .of ("acc_number", "branch_id", "balance") .collect (Collectors.toCollection (HashSet :: new))); public List safeFindAccountsByCustomerId (String customerId, String orderBy) kaster Undtagelse {String sql = "select" + "customer_id, acc_number, branch_id, balance from Accounts" + "where customer_id =?"; hvis (VALID_COLUMNS_FOR_ORDER_BY.contains (orderBy)) {sql = sql + "order efter" + orderBy; } ellers {kast nyt IllegalArgumentException ("Godt forsøg!"); } Forbindelse c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... resultat sæt behandling udeladt}

Her, vi kombinerer den forberedte erklæringsmetode og en hvidliste, der bruges til at desinficere rækkefølge argument. Det endelige resultat er en sikker streng med den endelige SQL-sætning. I dette enkle eksempel bruger vi et statisk sæt, men vi kunne også have brugt databasemetadatafunktioner til at oprette det.

Vi kan bruge den samme tilgang til JPA og drage også fordel af Criteria API og Metadata for at undgå at bruge Snor konstanter i vores kode:

// Kort over gyldige JPA-kolonner til sortering af det endelige kort VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (nyt AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), nyt AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), nyt AbstractMap.SimpleEntry (Konto.balance). Balance. (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); hvis (orderByAttribute == null) {smid nyt IllegalArgumentException ("Godt forsøg!"); } CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Root root = cq.from (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)) .orderBy (cb.asc (root.get (orderByAttribute))); TypedQuery q = em.createQuery (cq); // Udfør forespørgsel og returner kortlagte resultater (udeladt)

Denne kode har den samme grundlæggende struktur som i almindelig JDBC. Først bruger vi en hvidliste til at desinficere kolonnenavnet, hvorefter vi fortsætter med at oprette en Kriteriespørgsmål for at hente poster fra databasen.

3.4. Er vi sikre nu?

Lad os antage, at vi har brugt parametriserede forespørgsler og / eller hvidlister overalt. Kan vi nu gå til vores manager og garantere, at vi er i sikkerhed?

Nå ... ikke så hurtigt. Uden at overveje Turing's standsningsproblem er der andre aspekter, vi skal overveje:

  1. Lagrede procedurer: Disse er også tilbøjelige til problemer med SQL Injection; når det er muligt, bedes du anvende sanitet selv på værdier, der sendes til databasen via udarbejdede udsagn
  2. Udløsere: Samme problem som med procedureopkald, men endnu mere snigende, for nogle gange har vi ingen idé om, at de er der ...
  3. Usikre direkte objekthenvisninger: Selvom vores applikation er SQL-Injection-fri, er der stadig en risiko for, at der er tilknyttet denne sårbarhedskategori - hovedpunktet her er relateret til forskellige måder, en angriber kan narre applikationen på, så den returnerer poster, som han eller hun ikke skulle have adgang til - der er et godt snydeark om dette emne tilgængeligt på OWASPs GitHub-arkiv

Kort sagt, vores bedste mulighed her er forsigtighed. Mange organisationer bruger i dag et "rødt hold" nøjagtigt til dette. Lad dem udføre deres job, hvilket er nøjagtigt at finde eventuelle resterende sårbarheder.

4. Skadekontrolteknikker

Som en god sikkerhedspraksis skal vi altid implementere flere forsvarslag - et koncept kendt som forsvar i dybden. Hovedideen er, at selvom vi ikke er i stand til at finde alle mulige sårbarheder i vores kode - et almindeligt scenario, når vi beskæftiger os med ældre systemer - skal vi i det mindste forsøge at begrænse den skade, et angreb ville påføre.

Selvfølgelig ville dette være et emne for en hel artikel eller endda en bog, men lad os nævne et par mål:

  1. Anvend princippet om mindst privilegium: Begræns så meget som muligt privilegierne for den konto, der bruges til at få adgang til databasen
  2. Brug databasespecifikke tilgængelige metoder for at tilføje et ekstra beskyttelseslag; for eksempel har H2-databasen en session-niveau-indstilling, der deaktiverer alle bogstavelige værdier på SQL-forespørgsler
  3. Brug kortvarige legitimationsoplysninger: Få applikationen til at rotere databaseoplysninger ofte; en god måde at implementere dette på er ved hjælp af Spring Cloud Vault
  4. Log alt: Hvis applikationen gemmer kundedata, er dette et must; der er mange tilgængelige løsninger, der integreres direkte i databasen eller fungerer som en proxy, så i tilfælde af et angreb kan vi i det mindste vurdere skaden
  5. Brug WAF'er eller lignende opløsningsdetekteringsløsninger: de er typiske sortliste eksempler - normalt kommer de med en betydelig database med kendte angrebssignaturer og vil udløse en programmerbar handling efter detektion. Nogle inkluderer også in-JVM-agenter, der kan opdage indtrængen ved at anvende en vis instrumentering - den største fordel ved denne tilgang er, at en eventuel sårbarhed bliver meget lettere at rette, da vi har en fuld staksporing tilgængelig.

5. Konklusion

I denne artikel har vi dækket SQL Injection-sårbarheder i Java-applikationer - en meget alvorlig trussel mod enhver organisation, der afhænger af data for deres forretning - og hvordan man forhindrer dem ved hjælp af enkle teknikker.

Som sædvanlig er den fulde kode til denne artikel tilgængelig på Github.

Persistens bund

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN