SQL Injection og hvordan kan jeg forhindre det?

Utholdenhetstopp

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

1. Introduksjon

Til tross for at det er en av de mest kjente sårbarhetene, fortsetter SQL Injection å rangeres på toppen av den beryktede OWASP Top 10-listen - nå en del av den mer generelle Injeksjon klasse.

I denne opplæringen vil vi utforske vanlige kodingsfeil i Java som fører til et sårbart program og hvordan man kan unngå dem ved hjelp av API-ene som er tilgjengelige i JVMs standard kjøretidsbibliotek. Vi vil også dekke hvilke beskyttelser vi kan få ut av ORMer som JPA, Hibernate og andre, og hvilke blinde flekker vi fortsatt må bekymre deg for.

2. Hvordan applikasjoner blir sårbare for SQL-injeksjon?

Injiseringsangrep fungerer fordi den eneste måten å utføre en gitt beregning for mange applikasjoner er å generere kode dynamisk som i sin tur drives av et annet system eller komponent. Hvis vi i ferd med å generere denne koden bruker upålitelige data uten riktig sanitering, etterlater vi en åpen dør for hackere å utnytte.

Denne påstanden kan høres litt abstrakt ut, så la oss se på hvordan dette skjer i praksis med et lærebokeksempel:

public List unsafeFindAccountsByCustomerId (String customerId) kaster SQLException {// UNSAFE !!! IKKE GJØR DETTE !!! Streng sql = "velg" + "kunde_id, acc_nummer, gren_id, saldo" + "fra Kontoer der customer_id = '" + customerId + "'"; Tilkobling c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

Problemet med denne koden er åpenbar: vi har satt de Kunde IDVerdien i spørringen uten validering i det hele tatt. Ingenting ille vil skje hvis vi er sikre på at denne verdien bare kommer fra pålitelige kilder, men kan vi?

La oss forestille oss at denne funksjonen brukes i en REST API-implementering for en regnskap ressurs. Å utnytte denne koden er trivielt: alt vi trenger å gjøre er å sende en verdi som, når den sammenkobles med den faste delen av spørringen, endrer den tiltenkte oppførselen:

curl -X GET \ '// localhost: 8080 / accounts? customerId = abc% 27% 20or% 20% 271% 27 =% 271' \

Forutsatt at Kunde ID parameterverdien blir ukontrollert til den når vår funksjon, her er hva vi vil motta:

abc 'eller' 1 '=' 1

Når vi forbinder denne verdien med den faste delen, får vi den endelige SQL-setningen som skal utføres:

velg customer_id, acc_number, branch_id, balance fra kontoer der customerId = 'abc' eller '1' = '1'

Sannsynligvis ikke det vi har ønsket ...

En smart utvikler (er vi ikke alle?) Nå tenkt: “Det er dumt! Jeg ville aldri bruk streng sammenføyning for å lage et spørsmål som dette ”.

Ikke så fort ... Dette kanoniske eksemplet er dumt, men det er situasjoner der vi fortsatt kan trenge å gjøre det:

  • Komplekse spørsmål med dynamiske søkekriterier: legge til UNION-klausuler, avhengig av kriteriene som leveres av brukeren
  • Dynamisk gruppering eller bestilling: REST API-er brukt som backend til en GUI-datatabell

2.1. Jeg bruker JPA. Jeg er trygg, ikke sant?

Dette er en vanlig misforståelse. JPA og andre ORM-er lindrer oss fra å lage håndkodede SQL-setninger, men de vil ikke hindre oss i å skrive sårbar kode.

La oss se hvordan JPA-versjonen av forrige eksempel ser ut:

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 problemet vi har påpekt tidligere er også til stede her: vi bruker uvalidert inngang for å lage et JPA-spørsmål, så vi blir utsatt for samme slags utnyttelse her.

3. Forebyggingsteknikker

Nå som vi vet hva en SQL-injeksjon er, la oss se hvordan vi kan beskytte koden vår mot denne typen angrep. Her fokuserer vi på et par veldig effektive teknikker som er tilgjengelige på Java og andre JVM-språk, men lignende konsepter er tilgjengelige i andre miljøer, som PHP, .Net, Ruby og så videre.

For de som leter etter en komplett liste over tilgjengelige teknikker, inkludert databasespesifikke, har OWASP Project et SQL Injection Prevention Cheat Sheet, som er et bra sted å lære mer om emnet.

3.1. Parameteriserte spørsmål

Denne teknikken består i å bruke utarbeidede uttalelser med spørsmålstegnholderen (“?”) I spørsmålene våre når vi trenger å sette inn en brukeroppgitt verdi. Dette er veldig effektivt, og med mindre det er en feil i implementeringen av JDBC-driveren, immun mot utnyttelser.

La oss omskrive vår eksempelfunksjon for å bruke denne teknikken:

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

Her har vi brukt preparStatement () metoden tilgjengelig i Forbindelse eksempel for å få en PreparedStatement. Dette grensesnittet utvider det vanlige Uttalelse grensesnitt med flere metoder som lar oss trygt sette inn verdier som leveres av brukeren i et spørsmål før vi utfører det.

For JPA har vi en lignende funksjon:

Streng jql = "fra konto der customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Utfør spørring og returner kartlagte resultater (utelatt)

Når du kjører denne koden under Spring Boot, kan vi stille inn eiendommen logging.level.sql til DEBUG og se hva spørringen faktisk er bygget for å utføre denne operasjonen:

// Merk: Utdata formatert for å passe til skjerm [DEBUG] [SQL] velg konto0_.id som id1_0_, konto0_.acc_number som acc_numb2_0_, konto0_.balanse som saldo3_0_, konto0_.branch_id som branch_i4_0_, konto0_.kunde_id som kunde5_0_ konto fra .customer_id =?

Som forventet oppretter ORM-laget en forberedt uttalelse ved hjelp av en plassholder for Kunde ID parameter. Dette er det samme vi har gjort i JDBC-saken - men med noen få uttalelser mindre, noe som er hyggelig.

Som en bonus resulterer denne tilnærmingen vanligvis i et bedre resultat, siden de fleste databaser kan cache spørringsplanen knyttet til en utarbeidet uttalelse.

Vær oppmerksom at denne tilnærmingen bare fungerer for plassholdere som brukes somverdier. For eksempel kan vi ikke bruke plassholdere til å endre navnet på en tabell dynamisk:

// Dette FUNGER IKKE !!! PreparedStatement p = c.prepareStatement ("velg antall (*) fra?"); p.setString (1, tabellnavn);

Her hjelper ikke JPA heller:

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

I begge tilfeller får vi en kjøretidsfeil.

Hovedårsaken bak dette er selve naturen til en utarbeidet uttalelse: databaseservere bruker dem til å cache spørringsplanen som kreves for å trekke resultatsettet, som vanligvis er det samme for enhver mulig verdi. Dette gjelder ikke for tabellnavn og andre konstruksjoner som er tilgjengelige på SQL-språket, for eksempel kolonner som brukes i en rekkefølge etter klausul.

3.2. JPA Criteria API

Siden eksplisitt JQL-spørringsbygging er hovedkilden til SQL Injections, bør vi foretrekke bruk av JPAs Query API, når det er mulig.

For en rask start på dette API, se artikkelen om Hibernate Criteria-spørsmål. Også verdt å lese er vår artikkel om JPA Metamodel, som viser hvordan vi genererer metamodellklasser som vil hjelpe oss med å bli kvitt strengkonstanter som brukes til kolonnenavn - og kjøretidsfeilene som oppstår når de endres.

La oss skrive om vår JPA-spørringsmetode for å bruke Criteria API:

CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Rotrot = cq.fra (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)); TypedQuery q = em.createQuery (cq); // Utfør spørring og returner kartlagte resultater (utelatt)

Her har vi brukt flere kodelinjer for å få det samme resultatet, men oppsiden er det nå vi trenger ikke å bekymre oss for JQL-syntaks.

Et annet viktig poeng: til tross for ordlighetsgraden, Criteria API gjør det enklere og tryggere å lage komplekse spørringstjenester. For et komplett eksempel som viser hvordan du gjør det i praksis, ta en titt på tilnærmingen som brukes av JHipster-genererte applikasjoner.

3.3. Sanitering av brukerdata

Datasanitisering er en teknikk for å bruke et filter på brukerleverte data, slik at det kan brukes trygt av andre deler av applikasjonen vår. Implementeringen av et filter kan variere mye, men vi kan generelt klassifisere dem i to typer: hvitelister og svartelister.

Svartelister, som består av filtre som prøver å identifisere et ugyldig mønster, har vanligvis liten verdi i sammenheng med forebygging av SQL Injection - men ikke for påvisning! Mer om dette senere.

Hvitelisterderimot, fungerer spesielt bra når vi kan definere nøyaktig hva som er gyldig input.

La oss forbedre vår safeFindAccountsByCustomerId metode så nå kan den som ringer også spesifisere kolonnen som brukes til å sortere resultatsettet. Siden vi kjenner settet med mulige kolonner, kan vi implementere en hvitliste ved hjelp av et enkelt sett og bruke den til å desinfisere den mottatte parameteren:

privat statisk slutt Sett 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 Unntak {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 by" + orderBy; } annet {kast nytt IllegalArgumentException ("Fin prøve!"); } Tilkobling c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... resultatsettbehandling utelatt}

Her, Vi kombinerer den tilberedte uttalelsesmetoden og en hvitliste som brukes til å desinfisere rekkefølge etter argument. Det endelige resultatet er en sikker streng med den endelige SQL-setningen. I dette enkle eksemplet bruker vi et statisk sett, men vi kunne også ha brukt databasemetadatafunksjoner for å lage det.

Vi kan bruke samme tilnærming for JPA, og dra nytte av Criteria API og Metadata for å unngå bruk String konstanter i koden vår:

// Kart over gyldige JPA-kolonner for sortering av endelig kart VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (ny AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), ny AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), ny AbstractMap.SimpleEntry (Account_calance). Balanse. (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); if (orderByAttribute == null) {kast nytt IllegalArgumentException ("Fin prøve!"); } CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Rotrot = cq.fra (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)) .orderBy (cb.asc (root.get (orderByAttribute))); TypedQuery q = em.createQuery (cq); // Utfør spørring og returner kartlagte resultater (utelatt)

Denne koden har samme grunnleggende struktur som i vanlig JDBC. Først bruker vi en hvitliste til å desinfisere kolonnenavnet, og deretter fortsetter vi med å lage en Kriterier for å hente postene fra databasen.

3.4. Er vi trygge nå?

La oss anta at vi har brukt parametrerte spørsmål og / eller hvitelister overalt. Kan vi nå gå til sjefen vår og garantere at vi er trygge?

Vel ... ikke så fort. Uten å engang vurdere Turings stoppeproblem, er det andre aspekter vi må vurdere:

  1. Lagrede prosedyrer: Disse er også utsatt for problemer med SQL Injection; når det er mulig, vennligst bruk sanitæranlegg selv på verdier som vil bli sendt til databasen via utarbeidede uttalelser
  2. Utløsere: Samme problem som med prosedyreanrop, men enda mer lumsk fordi vi noen ganger ikke aner at de er der ...
  3. Usikre referanser til direkte objekter: Selv om applikasjonen vår er fri for SQL-Injection, er det fortsatt en risiko som er knyttet til denne sårbarhetskategorien - hovedpoenget her er relatert til forskjellige måter en angriper kan lure applikasjonen på, slik at den returnerer poster han eller hun ikke skulle ha tilgang til - det er et godt jukseark om dette emnet tilgjengelig på OWASPs GitHub-arkiv

Kort sagt, vårt beste alternativ her er forsiktighet. Mange organisasjoner bruker i dag et "rødt team" akkurat til dette. La dem gjøre jobben sin, som er å finne eventuelle gjenværende sårbarheter.

4. Skadekontrollteknikker

Som en god sikkerhetspraksis bør vi alltid implementere flere forsvarslag - et konsept kjent som forsvar i dybden. Hovedideen er at selv om vi ikke finner alle mulige sårbarheter i koden vår - et vanlig scenario når vi arbeider med eldre systemer - bør vi i det minste prøve å begrense skaden et angrep vil påføre.

Selvfølgelig vil dette være et tema for en hel artikkel eller til og med en bok, men la oss nevne noen tiltak:

  1. Bruk prinsippet om minst privilegium: Begrens så mye som mulig rettighetene til kontoen som brukes til å få tilgang til databasen
  2. Bruk databasespesifikke tilgjengelige metoder for å legge til et ekstra beskyttelseslag; for eksempel har H2-databasen et alternativ på øktnivå som deaktiverer alle bokstavelige verdier på SQL-spørringer
  3. Bruk kortvarig legitimasjon: Få applikasjonen til å rotere påloggingsinformasjonen ofte; en god måte å implementere dette på er å bruke Spring Cloud Vault
  4. Logg alt: Hvis applikasjonen lagrer kundedata, er dette et must; det er mange løsninger tilgjengelig som integreres direkte i databasen eller fungerer som proxy, så i tilfelle et angrep kan vi i det minste vurdere skaden
  5. Bruk WAF eller lignende påvisningsløsninger: de er typiske svarteliste eksempler - vanligvis kommer de med en betydelig database med kjente angrepssignaturer og vil utløse en programmerbar handling etter deteksjon. Noen inkluderer også in-JVM-agenter som kan oppdage inntrenging ved å bruke noen instrumentering - den største fordelen med denne tilnærmingen er at en eventuell sårbarhet blir mye lettere å fikse, siden vi vil ha en full stack-spor tilgjengelig.

5. Konklusjon

I denne artikkelen har vi dekket SQL Injection-sårbarheter i Java-applikasjoner - en veldig alvorlig trussel mot enhver organisasjon som er avhengig av data for deres virksomhet - og hvordan du kan forhindre dem ved hjelp av enkle teknikker.

Som vanlig er full kode for denne artikkelen tilgjengelig på Github.

Persistensbunn

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

$config[zx-auto] not found$config[zx-overlay] not found