SQL ubrizgavanje i kako ga spriječiti?

Vrh postojanosti

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ

1. Uvod

Iako je jedna od najpoznatijih ranjivosti, SQL Injection i dalje se nalazi na prvom mjestu zloglasnog popisa OWASP Top 10 - koji je sada dio općenitijih Injekcija razred.

U ovom uputstvu istražit ćemo uobičajene pogreške kodiranja u Javi koje dovode do ranjive aplikacije i kako ih izbjeći koristeći API-je dostupne u standardnoj runtime knjižnici JVM-a. Također ćemo pokriti koje zaštitne mjere možemo dobiti od ORM-ova poput JPA, Hibernate i drugih te oko kojih slijepih točaka još uvijek moramo brinuti.

2. Kako aplikacije postaju ranjive na ubrizgavanje SQL-a?

Napadi ubrizgavanjem djeluju jer je, za mnoge aplikacije, jedini način izvršavanja određenog računanja dinamičko generiranje koda koji zauzvrat pokreće drugi sustav ili komponenta. Ako u procesu generiranja ovog koda koristimo nepovjerljive podatke bez odgovarajuće sanacije, hakeri ostavljamo otvorena vrata za iskorištavanje.

Ova izjava možda zvuči pomalo apstraktno, pa pogledajmo kako se to događa u praksi s primjerom iz udžbenika:

javni popis unsafeFindAccountsByCustomerId (String customerId) baca SQLException {// UNSAFE !!! NE ČINI OVO !!! Niz sql = "select" + "customer_id, acc_number, branch_id, saldo" + "s Računa gdje je customer_id = '" + customerId + "'"; Veza c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

Problem s ovim kodom je očit: stavili smo the id kupcaVrijednost u upit bez provjere valjanosti. Ništa se loše neće dogoditi ako smo sigurni da će ta vrijednost dolaziti samo iz pouzdanih izvora, ali možemo li?

Zamislimo da se ova funkcija koristi u REST API implementaciji za račun resurs. Iskorištavanje ovog koda je trivijalno: sve što moramo učiniti je poslati vrijednost koja, kada se poveže s fiksnim dijelom upita, promijeni namjeravano ponašanje:

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

Pod pretpostavkom id kupca vrijednost parametra ostaje neprovjerena dok ne dosegne našu funkciju, evo što bismo dobili:

abc 'ili' 1 '=' 1

Kad pridružimo ovu vrijednost s fiksnim dijelom, dobit ćemo završni SQL izraz koji će se izvršiti:

odaberite customer_id, acc_number, branch_id, saldo s Računa gdje je customerId = 'abc' ili '1' = '1'

Vjerojatno nije ono što smo željeli ...

Pametni programer (nismo li svi?) Sada bi razmišljao: „To je glupo! Iskaznica nikada koristite spajanje nizova za izgradnju ovakvog upita ".

Ne tako brzo ... Ovaj je kanonski primjer zaista glup, ali postoje situacije u kojima bismo to možda još trebali učiniti:

  • Složeni upiti s dinamičkim kriterijima pretraživanja: dodavanje klauzula UNION ovisno o kriterijima koje je dostavio korisnik
  • Dinamično grupiranje ili redoslijed: REST API-ji koji se koriste kao pozadina za GUI tablicu podataka

2.1. Koristim JPA. Na sigurnom sam, zar ne?

Ovo je česta zabluda. JPA i drugi ORM-ovi oslobađaju nas stvaranja ručno kodiranih SQL izraza, ali oni to čine neće nas spriječiti da napišemo ranjivi kod.

Pogledajmo kako izgleda JPA verzija prethodnog primjera:

javni popis unsafeJpaFindAccountsByCustomerId (String customerId) {String jql = "s računa gdje je customerId = '" + customerId + "'"; TypedQuery q = em.createQuery (jql, Account.class); vrati q.getResultList () .stream () .map (this :: toAccountDTO) .collect (Collectors.toList ()); } 

Ovdje je prisutan i isti problem na koji smo ranije ukazivali: koristimo neovlašteni ulaz za stvaranje JPA upita, pa smo ovdje izloženi istoj eksploataciji.

3. Tehnike prevencije

Sad kad znamo što je SQL injekcija, pogledajmo kako možemo zaštititi svoj kod od ove vrste napada. Ovdje se usredotočujemo na nekoliko vrlo učinkovitih tehnika dostupnih u Javi i drugim JVM jezicima, ali slični koncepti dostupni su i u drugim okruženjima, kao što su PHP, .Net, Ruby i tako dalje.

Za one koji traže cjelovit popis dostupnih tehnika, uključujući one specifične za bazu podataka, OWASP projekt održava varalicu za sprečavanje ubrizgavanja SQL-a, što je dobro mjesto za naučiti više o toj temi.

3.1. Parametrirani upiti

Ova se tehnika sastoji od korištenja pripremljenih izjava s rezerviranim mjestom upitnika ("?") U našim upitima kad god trebamo umetnuti korisničku vrijednost. Ovo je vrlo učinkovito i, osim ako postoji pogreška u provedbi JDBC pokretačkog programa, imuno na eksploatacije.

Prepišimo našu primjernu funkciju kako bismo koristili ovu tehniku:

javni popis safeFindAccountsByCustomerId (String customerId) baca izuzetak {String sql = "select" + "customer_id, acc_number, branch_id, saldo s Računa" + "where customer_id =?"; Veza c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); ResultSet rs = p.executeQuery (sql)); // izostavljeno - obradite retke i vratite popis računa}

Ovdje smo koristili pripremaStatement () metoda dostupna u Veza primjer da biste dobili a PreparedStatement. Ovo sučelje proširuje redovno Izjava sučelje s nekoliko metoda koje nam omogućuju sigurno umetanje korisničkih vrijednosti u upit prije izvršenja.

Za JPA imamo sličnu značajku:

Niz jql = "s računa gdje je customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Izvršiti upit i vratiti mapirane rezultate (izostavljeno)

Kada pokrećemo ovaj kod pod Spring Boot, možemo postaviti svojstvo sječa.razina.sql DEBUG i vidjeti koji je upit zapravo izgrađen da bi se izvršila ova operacija:

// Napomena: Izlaz oblikovati kako bi odgovarao zaslonu [Debug] [SQL] odabir account0_.id kao id1_0_, account0_.acc_number kao acc_numb2_0_, account0_.balance kao balance3_0_, account0_.branch_id kao branch_i4_0_, account0_.customer_id kao customer5_0_ od računi account0_ gdje account0_ .customer_id =?

Kao što se i očekivalo, sloj ORM stvara pripremljeni izraz koristeći rezervirano mjesto za id kupca parametar. To je isto što smo učinili u običnom slučaju JDBC - ali s nekoliko izjava manje, što je lijepo.

Kao bonus, ovaj pristup obično rezultira upitom s boljom izvedbom, jer većina baza podataka može predmemorirati plan upita povezan s pripremljenom izjavom.

Molim Zabilježite da ovaj pristup djeluje samo za rezervirana mjesta koja se koriste kaovrijednosti. Na primjer, ne možemo koristiti rezervirana mjesta za dinamičku promjenu naziva tablice:

// Ovo NEĆE RADITI !!! PreparedStatement p = c.prepareStatement ("odaberite count (*) from?"); p.setString (1, tableName);

Ovdje vam neće pomoći ni JPA:

// Ovo NEĆE RADITI !!! Niz jql = "select count (*) from: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tableName", tableName); vratiti q.getSingleResult (); 

U oba slučaja dobit ćemo pogrešku u izvođenju.

Glavni razlog tome je sama priroda pripremljene izjave: poslužitelji baze podataka koriste ih za predmemoriranje plana upita potrebnog za povlačenje skupa rezultata, koji je obično isti za bilo koju moguću vrijednost. To ne vrijedi za nazive tablica i druge konstrukcije dostupne u SQL jeziku, kao što su stupci koji se koriste u poredak po klauzula.

3.2. API kriterija JPA

Budući da je eksplicitna izgradnja JQL upita glavni izvor SQL injekcija, trebali bismo favorizirati upotrebu JPA API-ja upita, kad je to moguće.

Kratke početne informacije o ovom API-ju potražite u članku o upitima Hibernate Criteria. Također vrijedi pročitati naš članak o JPA Metamodelu, koji pokazuje kako generirati klase metamodela koje će nam pomoći da se riješimo konstanti niza koje se koriste za nazive stupaca - i grešaka u izvođenju koje se pojavljuju kada se promijene.

Prepišimo našu metodu upita JPA kako bismo koristili API kriterija:

CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Korijenski korijen = cq.from (Account.class); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)); TypedQuery q = em.createQuery (cq); // Izvršiti upit i vratiti mapirane rezultate (izostavljeno)

Ovdje smo upotrijebili više redaka koda da bismo dobili isti rezultat, ali sada je to naviše ne moramo brinuti o JQL sintaksi.

Još jedna važna stvar: unatoč svojoj opširnosti, API kriterija čini stvaranje složenih usluga upita jednostavnijim i sigurnijim. Za cjelovit primjer koji pokazuje kako to učiniti u praksi, pogledajte pristup koji koriste aplikacije generirane od strane JHipster.

3.3. Sanitizacija korisničkih podataka

Sanitizacija podataka tehnika je primjene filtra na podatke koje je korisnik dostavio tako da ih mogu sigurno koristiti drugi dijelovi naše aplikacije. Implementacija filtra može se uvelike razlikovati, ali općenito ih možemo klasificirati u dvije vrste: bijele i crne liste.

Crne liste, koji se sastoje od filtara koji pokušavaju identificirati nevažeći obrazac, obično nemaju veliku vrijednost u kontekstu sprečavanja ubrizgavanja SQL-a - ali ne i za otkrivanje! O tome više kasnije.

Bijele liste, s druge strane, posebno dobro rade kada možemo točno definirati što je valjani ulaz.

Poboljšajmo naše safeFindAccountsByCustomerId metodom pa pozivatelj može također odrediti stupac koji se koristi za sortiranje skupa rezultata. Budući da znamo skup mogućih stupaca, bijeli popis možemo implementirati pomoću jednostavnog skupa i pomoću njega sanirati primljeni parametar:

privatni statički konačni skup VALID_COLUMNS_FOR_ORDER_BY = Collections.unmodifiableSet (Stream .of ("acc_number", "branch_id", "balance") .collect (Collectors.toCollection (HashSet :: new))); javni popis safeFindAccountsByCustomerId (String customerId, String orderBy) baca izuzetak {String sql = "select" + "customer_id, acc_number, branch_id, saldo s Računa" + "where customer_id =?"; if (VALID_COLUMNS_FOR_ORDER_BY.contens (orderBy)) {sql = sql + "poredak po" + orderBy; } else {baciti novi IllegalArgumentException ("Dobar pokušaj!"); } Veza c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... obrada skupa rezultata izostavljena}

Ovdje, kombiniramo pristup pripremljene izjave i bijelu listu koja se koristi za sanaciju narudžbaBy argument. Konačni rezultat je siguran niz sa završnim SQL izrazom. U ovom jednostavnom primjeru koristimo statički skup, ali za njegovo stvaranje mogli smo koristiti i funkcije metapodataka baze podataka.

Isti pristup možemo koristiti za JPA, također koristeći prednosti kriterija API-ja i metapodatke kako bismo izbjegli upotrebu Niz konstante u našem kodu:

// Karta važećih JPA stupaca za sortiranje konačne karte VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (new AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), new AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), new AbstractMap.SimpleEntry (Account_.balans. (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); if (orderByAttribute == null) {baciti novi IllegalArgumentException ("Dobar pokušaj!"); } CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Korijenski korijen = 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); // Izvršiti upit i vratiti mapirane rezultate (izostavljeno)

Ovaj kôd ima istu osnovnu strukturu kao u običnom JDBC. Prvo koristimo bijelu listu za sanaciju imena stupca, a zatim nastavljamo s izradom a KriterijiPitanje za preuzimanje zapisa iz baze podataka.

3.4. Jesmo li sigurni sada?

Pretpostavimo da smo svugdje koristili parametrizirane upite i / ili dopuštene popise. Možemo li sada otići do svog menadžera i jamčiti da smo sigurni?

Pa ... ne tako brzo. Bez čak i razmatranja Turingova zaustavljanja, postoje i drugi aspekti koje moramo uzeti u obzir:

  1. Pohranjeni postupci: Oni su također skloni problemima s ubrizgavanjem SQL-a; kad god je to moguće, primijenite sanitaciju čak i na vrijednosti koje će se poslati u bazu podataka putem pripremljenih izjava
  2. Okidači: Ista stvar kao i kod poziva procedura, ali još podmuklija jer ponekad nemamo pojma da su tamo ...
  3. Nesigurne reference izravnih objekata: Čak i ako je naša aplikacija besplatna za SQL-Injection, i dalje postoji rizik koji je povezan s ovom kategorijom ranjivosti - glavna stvar ovdje je povezana s različitim načinima na koje napadač može zavarati aplikaciju, pa vraća zapise koje on ili ona nije trebao imati pristup - postoji dobar varalica na ovu temu dostupna u OWASP-ovom GitHub spremištu

Ukratko, naša najbolja opcija ovdje je oprez. Mnoge organizacije danas za to koriste "crveni tim". Pustite ih da rade svoj posao, a to je točno da pronađu preostale ranjivosti.

4. Tehnike kontrole štete

Kao dobra sigurnosna praksa, uvijek bismo trebali provoditi više obrambenih slojeva - koncept poznat kao obrana u dubini. Glavna ideja je da, čak i ako ne možemo pronaći sve moguće ranjivosti u našem kodu - uobičajeni scenarij kada se radi sa naslijeđenim sustavima -, trebali bismo barem pokušati ograničiti štetu koju bi napad mogao nanijeti.

Naravno, ovo bi bila tema za cijeli članak ili čak knjigu, ali nabrojimo nekoliko mjera:

  1. Primijenite načelo najmanje povlastice: Ograničite što je više moguće privilegije računa koji se koristi za pristup bazi podataka
  2. Koristite dostupne metode specifične za bazu podataka kako biste dodali dodatni zaštitni sloj; na primjer, baza podataka H2 ima opciju na razini sesije koja onemogućava sve doslovne vrijednosti u SQL upitima
  3. Koristite kratkotrajne vjerodajnice: Neka aplikacija često rotira vjerodajnice baze podataka; dobar način da se to primijeni je korištenje Spring Cloud Vault-a
  4. Zapiši sve: Ako aplikacija pohranjuje podatke o kupcima, to je neophodno; dostupno je mnogo rješenja koja se izravno integriraju u bazu podataka ili rade kao proxy, tako da u slučaju napada možemo barem procijeniti štetu
  5. Koristite WAF ili slična rješenja za otkrivanje upada: to su tipična crna lista primjeri - obično dolaze s velikom bazom podataka poznatih potpisa napada i po otkrivanju pokreću programibilnu akciju. Neki također uključuju in-JVM agente koji mogu otkriti upade primjenom nekih instrumenata - glavna prednost ovog pristupa je što eventualnu ranjivost postaje puno lakše popraviti jer ćemo imati na raspolaganju puni trag steka.

5. Zaključak

U ovom smo članku pokrili ranjivosti SQL Injection u Java aplikacijama - vrlo ozbiljnu prijetnju bilo kojoj organizaciji koja ovisi o podacima o njihovom poslovanju - i kako ih spriječiti pomoću jednostavnih tehnika.

Kao i obično, puni kôd za ovaj članak dostupan je na Githubu.

Dno postojanosti

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ