SQL injekció és hogyan lehet megakadályozni?

Kitartás felső

Most jelentettem be az újat Tanulj tavaszt tanfolyam, amelynek középpontjában az 5. tavasz és a tavaszi bakancs 2 alapjai állnak:

>> ELLENŐRIZZE A FOLYAMATOT

1. Bemutatkozás

Annak ellenére, hogy az egyik legismertebb sebezhetőség, az SQL Injection továbbra is a hírhedt OWASP Top 10-es listájának első helyén áll - most része az általánosabbnak Injekció osztály.

Ebben az oktatóanyagban felfedezzük a Java általános kódolási hibái, amelyek sebezhető alkalmazáshoz vezetnek, és hogyan lehet ezeket elkerülni a JVM szabványos futásidejű könyvtárában elérhető API-k használatával. Kitérünk arra is, hogy milyen védelmet kaphatunk az olyan ORM-ekből, mint a JPA, a Hibernate és mások, és mely vakfoltok miatt kell még aggódnunk.

2. Hogyan válnak az alkalmazások sebezhetővé az SQL injekcióval szemben?

Az injekciós támadások azért működnek, mert sok alkalmazás esetében az adott számítás végrehajtásának egyetlen módja az a kód dinamikus előállítása, amelyet viszont egy másik rendszer vagy komponens futtat.. Ha a kód előállítása során megbízhatatlan adatokat használunk megfelelő fertőtlenítés nélkül, akkor nyitott kaput hagyunk a hackerek számára.

Ez az állítás kissé elvontnak tűnhet, ezért nézzük meg, hogyan történik ez a gyakorlatban egy tankönyvpéldával:

public List unsafeFindAccountsByCustomerId (String customerId) dobja az SQLException {// UNSAFE !!! NE Tegye ezt !!! String sql = "select" + "customer_id, acc_number, branch_id, balance" + "from Accounts where customer_id =" "+ customerId +" ""; C kapcsolat = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

A probléma ezzel a kóddal nyilvánvaló: tettük a Ügyfél-azonosítóÉrtékeket a lekérdezésbe, érvényesítés nélkül. Semmi rossz nem fog történni, ha biztosak vagyunk abban, hogy ez az érték csak megbízható forrásokból származik, de mi?

Képzeljük el, hogy ezt a függvényt egy REST API implementációban használják számla forrás. A kód kihasználása triviális: mindössze annyit kell tennünk, hogy elküldünk egy értéket, amely a lekérdezés fix részével összefűzve megváltoztatja a tervezett viselkedést:

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

Feltéve, hogy Ügyfél-azonosító A paraméter értéke nem ellenőrizhető, amíg el nem éri a függvényünket, a következőket kapjuk:

abc 'vagy' 1 '=' 1

Amikor ehhez az értékhez csatlakozunk a fix részhez, megkapjuk a végső SQL utasítást, amely végrehajtásra kerül:

válassza ki az ügyfél_azonosító, az acc_number, a fióktelep azonosítóját, az egyenleget az olyan számlákról, ahol customerId = 'abc' vagy '1' = '1'

Valószínűleg nem az, amit szerettünk volna ...

Egy okos fejlesztő (nem vagyunk valamennyien?) Most azt gondolná: „Ez butaság! Én soha használja a karaktersorozat összefűzését egy ilyen lekérdezés létrehozásához ”.

Nem olyan gyorsan ... Ez a kanonikus példa valóban buta, de vannak olyan helyzetek, amikor még szükségünk lehet rá:

  • Komplex lekérdezések dinamikus keresési feltételekkel: UNION-záradékok hozzáadása a felhasználó által megadott feltételektől függően
  • Dinamikus csoportosítás vagy sorrend: REST API-k, amelyeket GUI adattáblák háttérként használnak

2.1. A JPA-t használom. Biztonságban vagyok, igaz?

Ez egy általános tévhit. A JPA és más ORM-ek mentesítenek minket a kézzel kódolt SQL utasítások létrehozásától, de ezek nem akadályozza meg a sérülékeny kódot.

Lássuk, hogyan néz ki az előző példa JPA verziója:

public list unsafeJpaFindAccountsByCustomerId (String customerId) {String jql = "abból a fiókból, ahol customerId = '" + customerId + "'"; TypedQuery q = em.createQuery (jql, Account.class); return q.getResultList () .stream () .map (this :: toAccountDTO) .collect (Collectors.toList ()); } 

Ugyanaz a kérdés, amire korábban rámutattunk, itt is jelen van: validálatlan bevitelt használunk egy JPA-lekérdezés létrehozásához, tehát itt ugyanolyan kizsákmányolásnak vagyunk kitéve.

3. Megelőzési technikák

Most, hogy tudjuk, mi az SQL injekció, nézzük meg, hogyan védhetjük meg kódunkat az ilyen típusú támadásoktól. Itt néhány nagyon hatékony technikára összpontosítunk, amelyek elérhetők a Java-ban és más JVM-nyelveken, de hasonló fogalmak elérhetők más környezetekben is, mint például a PHP, .Net, Ruby és így tovább.

Azok számára, akik a rendelkezésre álló technikák teljes listáját keresik, beleértve az adatbázis-specifikusakat is, az OWASP Project fenntart egy SQL Injection Prevention Cheat Sheet-et, amely jó hely arra, hogy többet megtudjon a témáról.

3.1. Paraméterezett lekérdezések

Ez a technika abból áll, hogy lekérdezéseinkben előkészített utasításokat használunk a kérdőjel helyőrzőjével („?”), Amikor a felhasználó által megadott értéket kell beszúrnunk. Ez nagyon hatékony, és hacsak nincs hiba a JDBC illesztőprogram megvalósításában, immunis a kihasználásokra.

Írjuk át a példafunkciónkat a technika használatához:

public list safeFindAccountsByCustomerId (String customerId) dobja a {String sql = "select" + "customer_id, acc_number, branch_id, balance számlát a" + "ahol" customer_id =? "; C kapcsolat = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); ResultSet rs = p.executeQuery (sql)); // kihagyva - sorok feldolgozása és számlalista visszaadása}

Itt használtuk a preparStatement () módszerben elérhető Kapcsolat például a PreparedStatement. Ez az interfész kiterjeszti a szokásosat Nyilatkozat interfész számos módszerrel, amelyek lehetővé teszik számunkra, hogy a felhasználó által megadott értékeket biztonságosan illesszünk be egy lekérdezésbe, mielőtt végrehajtanánk.

A JPA esetében hasonló funkcióval rendelkezünk:

Karakterlánc jql = "abból a fiókból, ahol customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Lekérdezés végrehajtása és leképezett eredmények visszaadása (kihagyva)

Amikor ezt a kódot a Spring Boot alatt futtatjuk, beállíthatjuk a tulajdonságot naplózás.szint.sql DEBUG és megnézheti, hogy a lekérdezés valójában mi épül fel a művelet végrehajtásához:

// Megjegyzés: A kimenet formázva, hogy illeszkedjen a képernyőhöz [DEBUG] [SQL], válassza a account0_.id azonosítót id1_0_, account0_.acc_number mint acc_numb2_0_, account0_.balance mint egyenleg3_0_, account0_.branch_id mint branch_i4_0_, account0_.customer_id as customer5_0_0 fiókot .customer_id =?

A várakozásoknak megfelelően az ORM réteg elkészít egy utasításot az helyőrző használatával Ügyfél-azonosító paraméter. Ugyanezt tettük a sima JDBC esetben is, de néhány állítással kevesebbel, ami jó.

Bónuszként ez a megközelítés általában jobban teljesítő lekérdezést eredményez, mivel a legtöbb adatbázis gyorsítótárba helyezheti az elkészített utasításhoz társított lekérdezési tervet.

Kérjük, vegye figyelembe hogy ez a megközelítés csak az így használt helyőrzők esetében működikértékek. Például nem használhatunk helyőrzőket egy táblázat nevének dinamikus megváltoztatásához:

// Ez NEM MŰKÖDIK !!! PreparedStatement p = c.prepareStatement ("select count (*) from?"); p.setString (1, tableName);

Itt a JPA sem segít:

// Ez NEM MŰKÖDIK !!! Karakterlánc jql = "select count (*) from: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tableName", tableName); return q.getSingleResult (); 

Mindkét esetben futásidejű hibát kapunk.

Ennek fő oka az elkészített utasítás jellege: az adatbázis-kiszolgálók ezeket tárolják az eredményhalmaz lekéréséhez szükséges lekérdezési terv gyorsítótárában, amely általában megegyezik minden lehetséges értékkel. Ez nem igaz a táblák nevére és az SQL nyelvben elérhető egyéb konstrukciókra, például az Rendezés kikötés.

3.2. JPA Criteria API

Mivel az explicit JQL lekérdezésépítés az SQL injekciók fő forrása, lehetőség szerint támogatnunk kell a JPA Query API használatát.

Az API gyors bemutatásához olvassa el a Hibernate Criteria lekérdezésekről szóló cikket. Érdemes elolvasni a JPA Metamodelről szóló cikkünket is, amely bemutatja, hogyan hozhatunk létre metamodell osztályokat, amelyek segítenek megszabadulni az oszlopneveknél használt karakterlánc-állandóktól - és a futás közbeni hibáktól, amelyek a változás során jelentkeznek.

Írjuk át a JPA lekérdezési módszerünket a Criteria API használatára:

CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Gyökérgyökér = cq.from (Számlaosztály); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)); TypedQuery q = em.createQuery (cq); // Lekérdezés végrehajtása és leképezett eredmények visszaadása (kihagyva)

Itt több kódsort használtunk ugyanazon eredmény eléréséhez, de a fejlõdés most az nem kell aggódnunk a JQL szintaxisa miatt.

Egy másik fontos pont: a sokszínűség ellenére a Criteria API egyszerűbbé és biztonságosabbá teszi az összetett lekérdezési szolgáltatások létrehozását. Ha egy teljes példát mutat be a gyakorlatban, kérjük, tekintse meg a JHipster által generált alkalmazások által alkalmazott megközelítést.

3.3. Felhasználói adatok megtisztítása

Az adatkezelés olyan módszer, amellyel szűrőt alkalmazunk a felhasználó által megadott adatokra, így alkalmazásunk más részei biztonságosan felhasználhatják. A szűrők implementációja nagyban változhat, de általában két típusba sorolhatjuk őket: engedélyezőlisták és feketelisták.

Feketelisták, amelyek egy érvénytelen mintát azonosítani próbáló szűrőkből állnak, általában kevés értékkel bírnak az SQL Injection prevencióval összefüggésben - de nem az észlelés érdekében! Erről később.

Engedélyezőlistákmásrészt különösen jól működik, ha pontosan meghatározhatjuk, mi az érvényes bemenet.

Fokozzuk a mi safeFindAccountsByCustomerId metódust, így a hívó most megadhatja az eredményhalmaz rendezéséhez használt oszlopot is. Mivel ismerjük a lehetséges oszlopok halmazát, egy egyszerű halmaz segítségével implementálhatunk egy engedélyezőlistát, és ezzel felhasználhatjuk a kapott paraméter megtisztítását:

privát statikus végleges készlet 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) dobja a Kivételt {String sql = "select" + "customer_id, acc_number, branch_id, balance from Accounts" + "where customer_id =?"; if (VALID_COLUMNS_FOR_ORDER_BY.contains (orderBy)) {sql = sql + "rendezés" szerint + orderBy; } else {dobja be az új IllegalArgumentException-t ("Szép próbálkozás!"); } Kapcsolat c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, customerId); // ... az eredménykészlet feldolgozása elhagyva}

Itt, ötvözzük az elkészített nyilatkozati megközelítést és a fehérítőlistát, amelyet a Rendezés érv. A végeredmény egy biztonságos karakterlánc a végső SQL utasítással. Ebben az egyszerű példában statikus halmazot használunk, de létrehozásukhoz használhattunk volna adatbázis-metaadat-függvényeket is.

Ugyanezt a megközelítést alkalmazhatjuk a JPA számára is, a Criteria API és a Metadata előnyeit is kihasználva a használat elkerülése érdekében Húr konstansok a kódunkban:

// Érvényes JPA oszlopok térképe a végleges térkép rendezéséhez VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (új AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), új AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), új AbstractMap.SimpleEntry (új). (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); if (orderByAttribute == null) {dobjon új IllegalArgumentException-t ("Szép próbálkozás!"); } CriteriaBuilder cb = em.getCriteriaBuilder (); CriteriaQuery cq = cb.createQuery (Account.class); Gyökérgyökér = cq.from (Számla.osztály); cq.select (root) .where (cb.equal (root.get (Account_.customerId), customerId)) .orderBy (cb.asc (root.get (orderByAttribute))); TypedQuery q = em.createQuery (cq); // Lekérdezés végrehajtása és leképezett eredmények visszaadása (kihagyva)

Ennek a kódnak ugyanaz az alapstruktúrája, mint a sima JDBC-ben. Először egy fehér listát használunk az oszlop nevének megtisztítására, majd folytatjuk a CriteriaQuery hogy beolvassa a rekordokat az adatbázisból.

3.4. Most biztonságban vagyunk?

Tegyük fel, hogy mindenhol paraméterezett lekérdezéseket és / vagy engedélyezőlistákat használtunk. Most elmehetünk a menedzserünkhöz és garantálhatjuk, hogy biztonságban vagyunk?

Nos ... nem olyan gyorsan. Anélkül, hogy figyelembe vennénk Turing leállási problémáját, más szempontokat is figyelembe kell vennünk:

  1. Tárolt eljárások: Ezek hajlamosak az SQL injekcióval kapcsolatos problémákra is; amikor csak lehetséges, kérjük, alkalmazza a higiénés feltételeket még azokra az értékekre is, amelyeket az elkészített nyilatkozatokon keresztül továbbítanak az adatbázisba
  2. Kiváltók: Ugyanaz a kérdés, mint az eljáráshívásoknál, de még alattomosabb, mert néha fogalmunk sincs arról, hogy ott vannak ...
  3. Nem biztonságos közvetlen objektum hivatkozások: Még akkor is, ha alkalmazásunk ingyenes az SQL-Injection alkalmazásban, továbbra is fennáll annak a kockázata, hogy ehhez a biztonsági rés-kategóriához társul - a fő szempont itt a támadók különböző módjaihoz kapcsolódik, így az általa nem feltételezett rekordokat ad vissza. hozzáférés - van egy jó csalólap ebben a témában az OWASP GitHub adattárában

Röviden, a legjobb megoldásunk itt az óvatosság. Számos szervezet manapság pontosan erre használja a „vörös csapatot”. Hadd végezzék munkájukat, pontosan a fennmaradó sérülékenységek felkutatására.

4. Kárellenőrzési technikák

Jó biztonsági gyakorlatként mindig több védelmi réteget kell megvalósítanunk - néven ismert fogalom védekezés mélységében. A fő gondolat az, hogy még akkor is, ha nem találunk minden lehetséges biztonsági rést a kódunkban - ez egy általános forgatókönyv a régi rendszerek kezelésekor -, legalább meg kell próbálnunk korlátozni a támadás által okozott károkat.

Természetesen ez egy egész cikk vagy akár egy könyv témája lenne, de nevezzünk meg néhány intézkedést:

  1. Alkalmazza a legkevesebb kiváltság elvét: A lehető legnagyobb mértékben korlátozza az adatbázis eléréséhez használt fiók jogosultságait
  2. Használjon elérhető adatbázis-specifikus módszereket egy további védelmi réteg hozzáadásához; például a H2 adatbázis rendelkezik egy munkamenet szintű opcióval, amely letiltja az összes szó szerinti értéket az SQL lekérdezésekben
  3. Használjon rövid életű hitelesítő adatokat: Az alkalmazás gyakran fordítsa el az adatbázis hitelesítő adatait; ennek megvalósításának jó módja a Spring Cloud Vault használata
  4. Naplóz mindent: Ha az alkalmazás ügyféladatokat tárol, akkor ez kötelező; sok olyan megoldás áll rendelkezésre, amelyek közvetlenül integrálódnak az adatbázisba, vagy proxyként működnek, így támadás esetén legalább fel tudjuk mérni a károkat
  5. Használjon WAF-okat vagy hasonló behatolás-érzékelő megoldásokat: ezek a tipikusak feketelista példák - általában az ismert támadási aláírások jelentős adatbázisával érkeznek, és észlelésükkor programozható műveletet indítanak el. Néhány olyan JVM-ügynököt is tartalmaz, amely valamilyen műszer alkalmazásával felismerheti a behatolásokat - ennek a megközelítésnek az a fő előnye, hogy egy esetleges sérülékenységet sokkal könnyebb kijavítani, mivel rendelkezésünkre áll egy teljes verem nyomkövetés.

5. Következtetés

Ebben a cikkben kitértünk a Java-alkalmazások SQL Injection biztonsági réseire - ez nagyon komoly fenyegetést jelent minden szervezet számára, amely üzleti adatoktól függ - és arról, hogyan lehet őket egyszerű technikákkal megakadályozni.

Szokás szerint a cikk teljes kódja elérhető a Github oldalon.

Perzisztencia alsó

Most jelentettem be az újat Tanulj tavaszt tanfolyam, amelynek középpontjában az 5. tavasz és a tavaszi bakancs 2 alapjai állnak:

>> ELLENŐRIZZE A FOLYAMATOT