SQL Injection a ako tomu zabrániť?

Perzistencia hore

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ

1. Úvod

Aj napriek tomu, že je SQL Injection jednou z najznámejších zraniteľností, naďalej sa umiestňuje na prvom mieste v neslávne známom zozname OWASP Top 10 - teraz je súčasťou všeobecnejšej Injekcia trieda.

V tomto výučbe to preskúmame bežné chyby v kódovaní v prostredí Java, ktoré vedú k zraniteľnej aplikácii, a ako sa im vyhnúť pomocou API dostupných v štandardnej runtime knižnici JVM. Ďalej sa zameriame na to, aké ochrany môžeme získať z ORM, ako sú JPA, Hibernate a ďalšie, a ktorých slepých uhlov sa budeme musieť ešte obávať.

2. Ako sa aplikácie stávajú zraniteľnými voči SQL Injection?

Injekčné útoky fungujú, pretože pre mnoho aplikácií je jediným spôsobom, ako vykonať daný výpočet, dynamické generovanie kódu, ktorý je zase spustený iným systémom alebo komponentom.. Ak v procese generovania tohto kódu použijeme nedôveryhodné údaje bez náležitej dezinfekcie, necháme hackerom otvorené dvere na zneužitie.

Toto tvrdenie môže znieť trochu abstraktne, poďme sa teda pozrieť na to, ako sa to v praxi deje, na príklade učebnice:

verejný zoznam unsafeFindAccountsByCustomerId (String customerId) vyvolá SQLException {// NEBEZPEČNÉ !!! Nerobte to !!! Reťazec sql = "select" + "customer_id, acc_number, branch_id, balance" + "z účtov, kde customer_id = '" + customerId + "'"; Pripojenie c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

Problém s týmto kódom je zrejmý: dali sme the customerIdHodnotu do dotazu bez akejkoľvek validácie. Nič zlé sa nestane, ak sme si istí, že táto hodnota bude pochádzať iba z dôveryhodných zdrojov, ale môžeme?

Poďme si predstaviť, že táto funkcia sa používa v implementácii REST API pre účet zdroj. Využitie tohto kódu je triviálne: musíme iba poslať hodnotu, ktorá po zreťazení s pevnou časťou dotazu zmení jeho zamýšľané správanie:

curl -X ZÍSKAJTE \ '// localhost: 8080 / accounts? customerId = abc% 27% 20 alebo% 20% 271% 27 =% 271' \

Za predpokladu customerId hodnota parametra zostane nezačiarknutá, kým nedosiahne našu funkciu, tu by sme dostali:

abc 'alebo' 1 '=' 1

Keď spojíme túto hodnotu s pevnou časťou, dostaneme konečný príkaz SQL, ktorý sa vykoná:

vyberte customer_id, acc_number, branch_id, zostatok z účtov, kde customerId = 'abc' alebo '1' = '1'

Pravdepodobne to nie je to, čo sme chceli ...

Inteligentný vývojár (nie sme všetci?) By si teraz myslel: „To je hlúpe! Ja by som nikdy pomocou reťazenia reťazcov vytvorte takýto dotaz “.

Nie tak rýchlo ... Tento kanonický príklad je skutočne hlúpy, ale sú situácie, keď to možno budeme musieť urobiť:

  • Komplexné dotazy s kritériami dynamického vyhľadávania: pridávanie klauzúl UNION v závislosti od kritérií poskytnutých používateľom
  • Dynamické zoskupovanie alebo objednávanie: REST API používané ako backend k údajovej tabuľke GUI

2.1. Používam JPA. Som v bezpečí, však?

Toto je častá mylná predstava. JPA a ďalšie ORM nás odbremeňujú od vytvárania ručne kódovaných príkazov SQL, ale sú nezabráni nám v písaní zraniteľného kódu.

Pozrime sa, ako vyzerá verzia JPA predchádzajúceho príkladu:

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

Rovnaká otázka, na ktorú sme už poukazovali, je tu tiež: používame neplatný vstup na vytvorenie dotazu JPA, takže sme tu vystavení rovnakému druhu vykorisťovania.

3. Prevenčné techniky

Teraz, keď vieme, čo je injekcia SQL, pozrime sa, ako môžeme chrániť náš kód pred týmto druhom útoku. Tu sa zameriavame na niekoľko veľmi efektívnych techník dostupných v prostredí Java a ďalších jazykoch JVM, ale podobné koncepty sú k dispozícii aj v iných prostrediach, ako sú PHP, .Net, Ruby a tak ďalej.

Pre tých, ktorí hľadajú kompletný zoznam dostupných techník vrátane tých, ktoré sú špecifické pre databázu, udržuje projekt OWASP projekt Cheat Sheet SQL Injection Prevention, ktorý je dobrým miestom na získanie ďalších informácií o tejto téme.

3.1. Parametrizované dotazy

Táto technika spočíva v použití pripravených vyhlásení so zástupným znakom otáznika („?“) V našich dotazoch, kedykoľvek potrebujeme vložiť hodnotu dodanú používateľom. Je to veľmi efektívne a pokiaľ nie je chyba v implementácii ovládača JDBC, je imunný voči zneužitiu.

Prepíšme našu ukážkovú funkciu na použitie tejto techniky:

public List safeFindAccountsByCustomerId (String customerId) vyvolá výnimku {String sql = "select" + "customer_id, acc_number, branch_id, zostatok z účtov" + "kde customer_id =?"; Pripojenie c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); ResultSet rs = p.executeQuery (sql)); // vynechané - spracujte riadky a vráťte zoznam účtov}

Tu sme použili prepareStatement () metóda dostupná v Pripojenie napríklad získať a Pripravené vyhlásenie. Toto rozhranie rozširuje bežné Vyhlásenie rozhranie s niekoľkými metódami, ktoré nám umožňujú bezpečne vložiť do dotazu hodnoty poskytnuté používateľom pred jeho vykonaním.

Pre JPA máme podobnú funkciu:

Reťazec jql = "z účtu, kde customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Vykonať dopyt a vrátiť namapované výsledky (vynechané)

Pri spustení tohto kódu pod Spring Boot môžeme nastaviť vlastnosť logging.level.sql DEBUG a zistiť, aký dotaz je skutočne zostavený na vykonanie tejto operácie:

// Poznámka: Výstup naformátovaný tak, aby vyhovoval obrazovke [DEBUG] [SQL] vyberte účet0_.id ako id1_0_, účet0_.acc_number ako acc_numb2_0_, account0_.balance ako zostatok3_0_, account0_.branch_id ako pobočka_i4_0_, account0_.customer_id ako customer5_0_ z account0_count_ .customer_id =?

Ako sa dalo očakávať, vrstva ORM vytvorí pripravený príkaz pomocou zástupného symbolu pre customerId parameter. Toto je to isté, čo sme urobili v obyčajnom prípade JDBC - ale o pár vyhlásení menej, čo je pekné.

Ako bonus tento prístup zvyčajne vedie k lepšiemu výkonu dotazu, pretože väčšina databáz dokáže plán dotazov spojený s pripraveným príkazom uložiť do medzipamäte.

Vezmite prosím na vedomie že tento prístup funguje iba pre zástupné symboly použité akohodnoty. Nemôžeme napríklad použiť zástupné symboly na dynamickú zmenu názvu tabuľky:

// Toto NEFUNGUJE !!! PreparedStatement p = c.prepareStatement ("vyberte počet (*) z?"); p.setString (1, tableName);

Tu nepomôže ani JPA:

// Toto NEBUDE FUNGOVAŤ !!! Reťazec jql = "vyberte počet (*) z: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tableName", tableName); návrat q.getSingleResult (); 

V obidvoch prípadoch sa zobrazí chyba za behu.

Hlavným dôvodom je samotná podstata pripraveného vyhlásenia: databázové servery ich používajú na ukladanie do pamäte cache dotazového plánu potrebného na vytiahnutie množiny výsledkov, ktorá je zvyčajne rovnaká pre každú možnú hodnotu. To neplatí pre názvy tabuliek a iné konštrukcie dostupné v jazyku SQL, ako sú napríklad stĺpce použité v zoradiť podľa doložka.

3.2. API JPA Criteria

Pretože vytváranie explicitných dotazov JQL je hlavným zdrojom injekcií SQL, mali by sme, pokiaľ je to možné, uprednostniť použitie JPA Query API.

Rýchly úvod do tohto API nájdete v článku o dotazoch na kritériá dlhodobého spánku. Za prečítanie stojí aj náš článok o metamodeli JPA, ktorý ukazuje, ako generovať triedy metamodelov, ktoré nám pomôžu zbaviť sa konštantných reťazcov používaných pre názvy stĺpcov - a chýb za behu, ktoré vzniknú pri ich zmene.

Prepíšme našu metódu dotazu JPA na použitie rozhrania 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); // Vykonať dopyt a vrátiť namapované výsledky (vynechané)

Tu sme použili viac riadkov kódu, aby sme dosiahli rovnaký výsledok, ale výhodou je, že teraz nemusíme sa starať o syntax JQL.

Ďalším dôležitým bodom: napriek jeho výrečnosti vďaka rozhraniu Criteria API je vytváranie komplexných dotazovacích služieb priamočiarejšie a bezpečnejšie. Úplný príklad, ktorý ukazuje, ako to v praxi urobiť, nájdete v prístupe, ktorý používajú aplikácie generované JHipster.

3.3. Sanitácia údajov používateľa

Sanitizácia údajov je technika aplikácie filtra na údaje poskytnuté používateľom, aby ich bolo možné bezpečne použiť v iných častiach našej aplikácie. Implementácia filtra sa môže veľmi líšiť, ale môžeme ich všeobecne rozdeliť do dvoch typov: biele a čierne listiny.

Zoznamy zakázaných položiek, ktoré pozostávajú z filtrov, ktoré sa pokúšajú identifikovať neplatný vzor, ​​majú v kontexte prevencie SQL Injection zvyčajne malú hodnotu - nie však pre detekciu! Viac o tom neskôr.

Zoznamy povolených, na druhej strane fungujú obzvlášť dobre, keď môžeme presne definovať, čo je platný vstup.

Poďme vylepšiť naše safeFindAccountsByCustomerId metóda, takže volajúci môže tiež určiť stĺpec, ktorý sa použije na zoradenie výsledkovej sady. Pretože poznáme množinu možných stĺpcov, môžeme bielu listinu implementovať pomocou jednoduchej množiny a pomocou nej dezinfikovať prijatý parameter:

private static final Set 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) vyvolá výnimku {String sql = "select" + "customer_id, acc_number, branch_id, zostatok z účtov" + "kde customer_id =?"; if (VALID_COLUMNS_FOR_ORDER_BY.contains (orderBy)) {sql = sql + "order by" + orderBy; } else {hodit novu IllegalArgumentException ("Nice try!"); } Pripojenie c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... spracovanie sady výsledkov vynechané}

Tu, kombinujeme prístup pripraveného vyhlásenia a bielu listinu použitú na sanitáciu zoradiť podľa argument. Konečným výsledkom je bezpečný reťazec s konečným príkazom SQL. V tomto jednoduchom príklade používame statickú množinu, ale na jej vytvorenie sme mohli použiť aj funkcie metadát databázy.

Rovnaký prístup môžeme použiť aj pre JPA, pričom sa tiež vyhýbame používaniu výhod rozhrania Criteria API a metadát String konštanty v našom kóde:

// Mapa platných stĺpcov JPA na triedenie výslednej mapy VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (nový AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), nový AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), nový AbstractMap.SimpleEntry (Account_.BALANCE). (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); if (orderByAttribute == null) {hodiť novú IllegalArgumentException ("Pekný pokus!"); } 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); // Vykonať dopyt a vrátiť namapované výsledky (vynechané)

Tento kód má rovnakú základnú štruktúru ako v obyčajnom JDBC. Najskôr pomocou whitelistu dezinfikujeme názov stĺpca, potom pokračujeme vytvorením a CriteriaQuery načítať záznamy z databázy.

3.4. Sme teraz v bezpečí?

Predpokladajme, že sme všade používali parametrizované dotazy alebo biele listiny. Môžeme teraz ísť k nášmu manažérovi a zaručiť, že sme v bezpečí?

No ... nie tak rýchlo. Bez toho, aby sme zvážili Turingov problém zastavenia, musíme zvážiť ďalšie aspekty:

  1. Uložené procedúry: Sú tiež náchylné na problémy s SQL Injection; vždy, keď je to možné, použite sanitáciu aj na hodnoty, ktoré sa do databázy odošlú prostredníctvom pripravených výkazov
  2. Spúšťače: Rovnaký problém ako pri volaní procedúr, ale o to zákernejší, že niekedy vôbec netušíme, že sú tam ...
  3. Nezabezpečené priame odkazy na objekty: Aj keď je naša aplikácia bez SQL Injection, stále existuje riziko spojené s touto kategóriou zraniteľnosti - hlavný bod tu súvisí s rôznymi spôsobmi, ako môže útočník oklamať aplikáciu, takže vracia záznamy, ktoré nemal mať prístup k - v repozitári GitHub OWASP je k dispozícii dobrý cheat sheet o tejto téme

Stručne povedané, našou najlepšou možnosťou je tu opatrnosť. Mnoho organizácií dnes používa presne „červený tím“. Nechajte ich robiť si prácu, ktorá spočíva práve v hľadaní zvyšných zraniteľností.

4. Techniky kontroly poškodenia

Ako dobrý bezpečnostný postup by sme mali vždy implementovať viac ochranných vrstiev - koncept známy ako obrana do hĺbky. Hlavná myšlienka je, že aj keď v našom kóde nedokážeme nájsť všetky možné chyby - bežný scenár pri práci so staršími systémami - mali by sme sa pokúsiť aspoň obmedziť škody, ktoré by útok spôsobil.

Toto by samozrejme bola téma pre celý článok alebo dokonca knihu, ale vymenujme niekoľko opatrení:

  1. Použite zásadu najmenších privilégií: Čo najviac obmedzte oprávnenia účtu použitého na prístup do databázy
  2. Použite dostupné metódy špecifické pre databázu na pridanie ďalšej ochrannej vrstvy; napríklad databáza H2 má možnosť na úrovni relácie, ktorá zakáže všetky doslovné hodnoty v dotazoch SQL
  3. Použite krátkodobé poverenia: Umožnite aplikácii často striedať poverenia databázy; dobrý spôsob, ako to implementovať, je použitie Spring Cloud Vault
  4. Zaznamenať všetko: Ak aplikácia ukladá údaje o zákazníkoch, je to nevyhnutné; existuje veľa riešení, ktoré sa integrujú priamo do databázy alebo fungujú ako proxy, takže v prípade útoku môžeme prinajmenšom posúdiť poškodenie
  5. Použite WAF alebo podobné riešenia detekcie vniknutia: sú typické čierna listina príklady - zvyčajne prichádzajú s rozsiahlou databázou známych signatúr útoku a po detekcii spustia programovateľnú akciu. Niektoré tiež zahŕňajú agentov v JVM, ktorí dokážu detekovať vniknutie použitím nejakého prístrojového vybavenia - hlavnou výhodou tohto prístupu je, že prípadnú zraniteľnosť je možné oveľa ľahšie opraviť, pretože budeme mať k dispozícii úplnú stopu.

5. Záver

V tomto článku sme sa venovali zraniteľnostiam aplikácií SQL Injection v aplikáciách Java - čo je veľmi vážna hrozba pre každú organizáciu, ktorá závisí od údajov o ich podnikaní - a ako im zabrániť pomocou jednoduchých techník.

Celý kód tohto článku je ako obvykle k dispozícii na stránkach Github.

Perzistencia dno

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ