SQL-injectie en hoe dit te voorkomen?

Persistentie top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Inleiding

Ondanks dat het een van de bekendste kwetsbaarheden is, blijft SQL Injection bovenaan de beruchte OWASP Top 10-lijst staan ​​- nu onderdeel van de meer algemene Injectie klasse.

In deze tutorial gaan we verkennen veelvoorkomende codeerfouten in Java die leiden tot een kwetsbare applicatie en hoe deze te vermijden met behulp van de API's die beschikbaar zijn in de standaard runtime-bibliotheek van de JVM. We bespreken ook welke beveiligingen we uit ORM's kunnen halen, zoals JPA, Hibernate en andere, en over welke blinde vlekken we ons nog zorgen moeten maken.

2. Hoe worden applicaties kwetsbaar voor SQL-injectie?

Injectie-aanvallen werken omdat, voor veel toepassingen, de enige manier om een ​​bepaalde berekening uit te voeren, is door dynamisch code te genereren die op zijn beurt wordt uitgevoerd door een ander systeem of onderdeel. Als we bij het genereren van deze code niet-vertrouwde gegevens gebruiken zonder de juiste opschoning, laten we een deur open voor hackers om misbruik te maken.

Deze uitspraak klinkt misschien wat abstract, dus laten we eens kijken hoe dit in de praktijk gebeurt met een schoolvoorbeeld:

openbare lijst unsafeFindAccountsByCustomerId (String customerId) genereert SQLException {// UNSAFE !!! DOE DIT NIET !!! String sql = "select" + "customer_id, acc_number, branch_id, saldo" + "van Accounts waar customer_id = '" + customerId + "'"; Verbinding c = dataSource.getConnection (); ResultSet rs = c.createStatement (). ExecuteQuery (sql); // ...}

Het probleem met deze code is duidelijk: we hebben gezet de Klanten ID'S waarde in de zoekopdracht zonder helemaal geen validatie. Er zal niets ergs gebeuren als we zeker weten dat deze waarde alleen uit betrouwbare bronnen komt, maar kunnen we?

Laten we ons voorstellen dat deze functie wordt gebruikt in een REST API-implementatie voor een account bron. Het misbruiken van deze code is triviaal: het enige wat we hoeven te doen is een waarde te verzenden die, wanneer deze wordt samengevoegd met het vaste deel van de query, het beoogde gedrag verandert:

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

Ervan uitgaande dat het Klanten ID parameterwaarde blijft uitgeschakeld totdat deze onze functie bereikt, dit is wat we zouden ontvangen:

abc 'of' 1 '=' 1

Wanneer we deze waarde samenvoegen met het vaste deel, krijgen we de laatste SQL-instructie die zal worden uitgevoerd:

selecteer customer_id, acc_number, branch_id, balance from Accounts where customerId = 'abc' of '1' = '1'

Waarschijnlijk niet wat we wilden ...

Een slimme ontwikkelaar (zijn we niet allemaal?) Zou nu denken: “Dat is gek! ID kaart nooit gebruik tekenreeksen om een ​​query als deze op te bouwen ”.

Niet zo snel ... Dit canonieke voorbeeld is inderdaad dom, maar er zijn situaties waarin we het misschien nog moeten doen:

  • Complexe zoekopdrachten met dynamische zoekcriteria: UNION-clausules toevoegen, afhankelijk van door de gebruiker aangeleverde criteria
  • Dynamisch groeperen of ordenen: REST API's die worden gebruikt als back-end voor een GUI-gegevenstabel

2.1. Ik gebruik JPA. Ik ben veilig, toch?

Dit is een veel voorkomende misvatting. JPA en andere ORM's verlost ons van het maken van handgecodeerde SQL-statements, maar dat zijn ze zal ons er niet van weerhouden kwetsbare code te schrijven.

Laten we eens kijken hoe de JPA-versie van het vorige voorbeeld eruitziet:

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

Hetzelfde probleem dat we eerder hebben genoemd, is ook hier aanwezig: we gebruiken niet-gevalideerde invoer om een ​​JPA-query te maken, dus we worden hier blootgesteld aan dezelfde soort exploit.

3. Preventietechnieken

Nu we weten wat een SQL-injectie is, gaan we kijken hoe we onze code kunnen beschermen tegen dit soort aanvallen. Hier concentreren we ons op een aantal zeer effectieve technieken die beschikbaar zijn in Java en andere JVM-talen, maar vergelijkbare concepten zijn beschikbaar voor andere omgevingen, zoals PHP, .Net, Ruby enzovoort.

Voor degenen die op zoek zijn naar een complete lijst van beschikbare technieken, inclusief database-specifieke technieken, houdt het OWASP-project een SQL Injection Prevention Cheat Sheet bij, wat een goede plek is om meer over het onderwerp te leren.

3.1. Geparametriseerde zoekopdrachten

Deze techniek bestaat uit het gebruik van voorbereide uitspraken met de plaatsaanduiding voor het vraagteken ("?") In onze zoekopdrachten wanneer we een door de gebruiker aangeleverde waarde moeten invoegen. Dit is zeer effectief en, tenzij er een bug in de implementatie van het JDBC-stuurprogramma zit, immuun voor exploits.

Laten we onze voorbeeldfunctie herschrijven om deze techniek te gebruiken:

openbare lijst safeFindAccountsByCustomerId (String customerId) genereert uitzondering {String sql = "select" + "customer_id, acc_number, branch_id, saldo van Accounts" + "where customer_id =?"; Verbinding c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, klant-id); ResultSet rs = p.executeQuery (sql)); // weggelaten - verwerk rijen en retourneer een accountlijst}

Hier hebben we de preparStatement () methode beschikbaar in de Verbinding instantie om een PreparedStatement. Deze interface breidt de reguliere Uitspraak interface met verschillende methoden waarmee we veilig door de gebruiker geleverde waarden in een query kunnen invoegen voordat deze wordt uitgevoerd.

Voor JPA hebben we een vergelijkbare functie:

String jql = "van Account waar customerId =: customerId"; TypedQuery q = em.createQuery (jql, Account.class) .setParameter ("customerId", customerId); // Voer query uit en retourneer toegewezen resultaten (weggelaten)

Als deze code wordt uitgevoerd onder Spring Boot, kunnen we de eigenschap instellen logging.level.sql naar DEBUG en kijk welke query daadwerkelijk is gebouwd om deze bewerking uit te voeren:

// Opmerking: uitvoer opgemaakt om in scherm [DEBUG] [SQL] te passen, selecteer account0_.id als id1_0_, account0_.acc_number als acc_numb2_0_, account0_.balance als saldo3_0_, account0_.branch_id als branch_i4_0_, account0_.customer_id als customer_5_0_ waar account0 van accounts .customer_id =?

Zoals verwacht, maakt de ORM-laag een voorbereide instructie met behulp van een tijdelijke aanduiding voor de Klanten ID parameter. Dit is hetzelfde dat we hebben gedaan in het gewone JDBC-geval - maar met een paar uitspraken minder, wat prettig is.

Als een bonus resulteert deze benadering meestal in een beter presterende query, aangezien de meeste databases het queryplan kunnen opslaan dat is gekoppeld aan een voorbereide instructie.

Houd er rekening mee dat dat deze benadering alleen werkt voor tijdelijke aanduidingen die worden gebruikt alswaarden. We kunnen bijvoorbeeld geen tijdelijke aanduidingen gebruiken om de naam van een tabel dynamisch te wijzigen:

// Dit gaat niet werken !!! PreparedStatement p = c.prepareStatement ("selecteer count (*) van?"); p.setString (1, tableName);

Hier helpt JPA ook niet:

// Dit ZAL NIET WERKEN !!! String jql = "selecteer aantal (*) uit: tableName"; TypedQuery q = em.createQuery (jql, Long.class) .setParameter ("tableName", tableName); retourneer q.getSingleResult (); 

In beide gevallen krijgen we een runtime-fout.

De belangrijkste reden hiervoor is de aard van een voorbereide instructie: databaseservers gebruiken ze om het queryplan te cachen dat nodig is om de resultatenset op te halen, wat meestal hetzelfde is voor elke mogelijke waarde. Dit geldt niet voor tabelnamen en andere constructies die beschikbaar zijn in de SQL-taal, zoals kolommen die worden gebruikt in een bestellen door clausule.

3.2. API voor JPA-criteria

Aangezien expliciete JQL-queryopbouw de belangrijkste bron van SQL-injecties is, moeten we, indien mogelijk, het gebruik van de Query-API van de JPA bevorderen.

Raadpleeg het artikel over Hibernate Criteria-queries voor een snelle introductie van deze API. Ook het lezen waard is ons artikel over JPA Metamodel, dat laat zien hoe metamodelklassen kunnen worden gegenereerd die ons zullen helpen om stringconstanten die worden gebruikt voor kolomnamen te verwijderen - en de runtime-bugs die optreden wanneer ze veranderen.

Laten we onze JPA-zoekmethode herschrijven om de Criteria API te gebruiken:

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); // Voer query uit en retourneer toegewezen resultaten (weggelaten)

Hier hebben we meer coderegels gebruikt om hetzelfde resultaat te krijgen, maar het voordeel is dat nu we hoeven ons geen zorgen te maken over de JQL-syntaxis.

Nog een belangrijk punt: ondanks zijn breedsprakigheid, de Criteria API maakt het maken van complexe zoekservices eenvoudiger en veiliger. Voor een compleet voorbeeld dat laat zien hoe u dit in de praktijk moet doen, kunt u kijken naar de aanpak die wordt gebruikt door door JHipster gegenereerde applicaties.

3.3. Opschonen van gebruikersgegevens

Gegevensopschoning is een techniek waarbij een filter wordt toegepast op door de gebruiker verstrekte gegevens, zodat deze veilig kan worden gebruikt door andere delen van onze applicatie. De implementatie van een filter kan erg variëren, maar we kunnen ze over het algemeen in twee typen indelen: witte lijsten en zwarte lijsten.

Zwarte lijsten, die bestaan ​​uit filters die een ongeldig patroon proberen te identificeren, zijn meestal van weinig waarde in de context van SQL Injection-preventie - maar niet voor de detectie! Hierover later meer.

Witte lijsten, aan de andere kant, werken bijzonder goed wanneer we kunnen precies definiëren wat een geldige invoer is.

Laten we onze safeFindAccountsByCustomerId methode, zodat de beller nu ook de kolom kan specificeren die wordt gebruikt om de resultatenset te sorteren. Omdat we de reeks mogelijke kolommen kennen, kunnen we een witte lijst implementeren met behulp van een eenvoudige set en deze gebruiken om de ontvangen parameter op te schonen:

private static final Set VALID_COLUMNS_FOR_ORDER_BY = Collections.unmodifiableSet (Stream .of ("acc_number", "branch_id", "balance") .collect (Collectors.toCollection (HashSet :: new))); openbare lijst safeFindAccountsByCustomerId (String customerId, String orderBy) genereert uitzondering {String sql = "select" + "customer_id, acc_number, branch_id, saldo van Accounts" + "where customer_id =?"; if (VALID_COLUMNS_FOR_ORDER_BY.contains (orderBy)) {sql = sql + "order by" + orderBy; } else {gooi nieuwe IllegalArgumentException ("Leuk geprobeerd!"); } Verbinding c = dataSource.getConnection (); PreparedStatement p = c.prepareStatement (sql); p.setString (1, klant-id); // ... resultaatsetverwerking weggelaten}

Hier, we combineren de voorbereide statement-aanpak en een witte lijst die wordt gebruikt om het orderBy argument. Het uiteindelijke resultaat is een veilige string met de laatste SQL-instructie. In dit eenvoudige voorbeeld gebruiken we een statische set, maar we hadden ook databasemetadatafuncties kunnen gebruiken om deze te maken.

We kunnen dezelfde aanpak gebruiken voor JPA, waarbij we ook profiteren van de Criteria API en Metadata om gebruik te vermijden Draad constanten in onze code:

// Kaart met geldige JPA-kolommen voor het sorteren van de definitieve kaart VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of (nieuwe AbstractMap.SimpleEntry (Account_.ACC_NUMBER, Account_.accNumber), nieuwe AbstractMap.SimpleEntry (Account_.BRANCH_ID, Account_.branchId), nieuwe AbstractMap.SimpleEntry, Account. (Collectors.toMap (Map.Entry :: getKey, Map.Entry :: getValue)); SingularAttribute orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get (orderBy); if (orderByAttribute == null) {throw new IllegalArgumentException ("Leuk geprobeerd!"); } 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); // Voer query uit en retourneer toegewezen resultaten (weggelaten)

Deze code heeft dezelfde basisstructuur als in de gewone JDBC. Eerst gebruiken we een witte lijst om de kolomnaam op te schonen, daarna maken we een CriteriaQuery om de records uit de database op te halen.

3.4. Zijn we nu veilig?

Laten we aannemen dat we overal geparametriseerde query's en / of witte lijsten hebben gebruikt. Kunnen we nu naar onze manager gaan en garanderen dat we veilig zijn?

Nou ... niet zo snel. Zonder zelfs maar het stopprobleem van Turing in overweging te nemen, zijn er andere aspecten die we moeten overwegen:

  1. Opgeslagen procedures: Deze zijn ook vatbaar voor problemen met SQL-injectie; Pas waar mogelijk sanitaire voorzieningen toe, zelfs op waarden die via voorbereide verklaringen naar de database worden gestuurd
  2. Triggers: Hetzelfde probleem als bij procedure-oproepen, maar nog verraderlijker omdat we soms geen idee hebben dat ze er zijn ...
  3. Onveilige verwijzingen naar directe objecten: Zelfs als onze applicatie SQL-Injection-vrij is, is er nog steeds een risico verbonden aan deze kwetsbaarheidscategorie - het belangrijkste punt hier is gerelateerd aan verschillende manieren waarop een aanvaller de applicatie kan misleiden, dus het retourneert records die hij of zij niet had mogen hebben toegang tot - er is een goed spiekbriefje over dit onderwerp beschikbaar in de GitHub-repository van OWASP

Kortom, onze beste optie hier is voorzichtigheid. Veel organisaties gebruiken hier tegenwoordig juist een “rood team” voor. Laat ze hun werk doen, en dat is precies het vinden van eventuele resterende kwetsbaarheden.

4. Schadebeheersingstechnieken

Als goede beveiligingspraktijk moeten we altijd meerdere verdedigingslagen implementeren - een concept dat bekend staat als verdediging in de diepte. Het belangrijkste idee is dat zelfs als we niet alle mogelijke kwetsbaarheden in onze code kunnen vinden - een veelvoorkomend scenario bij het omgaan met legacysystemen - we in ieder geval moeten proberen de schade die een aanval zou toebrengen te beperken.

Dit zou natuurlijk een onderwerp zijn voor een heel artikel of zelfs een boek, maar laten we een paar maatregelen noemen:

  1. Pas het principe van de minste rechten toe: Beperk zoveel mogelijk de privileges van het account dat wordt gebruikt om toegang te krijgen tot de database
  2. Gebruik databasespecifieke methoden die beschikbaar zijn om een ​​extra beschermingslaag toe te voegen; De H2-database heeft bijvoorbeeld een optie op sessieniveau die alle letterlijke waarden voor SQL-query's uitschakelt
  3. Gebruik kortstondige inloggegevens: Laat de toepassing databasereferenties vaak roteren; een goede manier om dit te implementeren is door Spring Cloud Vault te gebruiken
  4. Log alles: Als de applicatie klantgegevens opslaat, is dit een must; er zijn veel oplossingen beschikbaar die direct in de database integreren of als proxy werken, dus in geval van een aanval kunnen we in ieder geval de schade inschatten
  5. Gebruik WAF's of vergelijkbare oplossingen voor inbraakdetectie: dat zijn de typische zwarte lijst voorbeelden - meestal worden ze geleverd met een omvangrijke database met bekende aanvalshandtekeningen en zullen ze bij detectie een programmeerbare actie activeren. Sommige bevatten ook in-JVM-agents die indringers kunnen detecteren door een aantal instrumenten toe te passen - het belangrijkste voordeel van deze aanpak is dat een eventuele kwetsbaarheid veel gemakkelijker op te lossen is omdat we een volledige stacktracering beschikbaar hebben.

5. Conclusie

In dit artikel hebben we de kwetsbaarheden van SQL-injectie in Java-applicaties besproken - een zeer ernstige bedreiging voor elke organisatie die afhankelijk is van gegevens voor hun bedrijf - en hoe deze kunnen worden voorkomen met behulp van eenvoudige technieken.

Zoals gewoonlijk is de volledige code voor dit artikel beschikbaar op Github.

Persistentie onderaan

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS