SQL-injektiot ovat ikäviä mutta niiltä voi välttyä
SQL-injektio on vakava haavoittuvuus jolla on lähes poikkeuksetta karmaisevat seuraukset. Haavoittuvuus on myös valitettavan yleinen sellaisissa koodikannoissa, jossa tietokantakyselytä rakennetaan raakoina merkkijonoina.
Hyvä puoli on se, että koodi on mahdollista rakentaa niin että SQL-injektion riski saadaan lähes nollaan. Lisäksi on pari kovennusta mitä voi tehdä kaiken varalta riskin pienentämiseksi.
Tärkein ensin
Ennen kuin mennään tästä pidemmälle, tehdään yksi asia selväksi. Jos SQL-injektioilta haluaa välttyä, tulee koodi rakentaa (tai refaktoroida jos se on jo rakennettu) niin, ettei raakoja SQL-kyselyitä parameterisoida lainkaan.
Koodissa ei saa olla mitään tämän näköistä:
var query = 'SELECT * FROM users WHERE id=' + '"' + sanitizeSqlParameter(req.params['id']) + '"';
Muuten on vain ajan kysymys milloin yhdessä kohdassa koodia tapahtuu yksi lipsahdus joka vaarantaa koko sovelluksen.
Pysytään siis 100-prosenttisesti turvallisissa menetelmissä. Käydään nämä seuraavaksi.
Paras ratkaisu: ORM (Object Relational Mapper) -kirjasto
ORM (Object-Relational Mapping) tarjoaa kehittäjille mahdollisuuden käsitellä tietokantatoimintoja raa'an SQL:n sijasta ihan vain koodilla ja olioilla. Nimi tulee siitä että relaatiotietokannan tietorakenteet "mapitetaan" luokkiin ohjelmakoodissa.
Esimerkki haavoittuvasta koodista
Oletetaan että meillä on sovellus jossa voidaan hakea käyttäjä tietokannasta käyttäjätunnuksen perusteella.
Tässä on esimerkki väärästä tavasta (koodissa on SQL-injektio haavoittuvuus):
def hae_kayttaja(kayttajanimi):
query = f"SELECT * FROM users WHERE username = '{kayttajanimi}'"
cursor.execute(query)
kayttaja = cursor.fetchone()
return kayttaja
Esimerkki turvallisesta koodista
Jos käytetään ORM:ää niin tietokantarakenne on kuvattuna koodissa, esimerkiksi seuraavanlaisella tavalla:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String, unique=True)
email = Column(String)
Sama funktio (joka hakee käyttäjän nimimerkillä) voidaan sitten toteuttaa tällä tavalla:
def hae_kayttaja(kayttajanimi):
kayttaja = session.query(User).filter_by(username=kayttajanimi).first()
return kayttaja
Huomaa että tällä kertaa kehittäjän ei tarvinnut edes osata SQL:ää, saati kirjoittaa sitä. Tietokantarakenne on selkeä ja nähtävissä heti koodista, ja luokat on tyypitetty niin, että kehitysympäristö herjaisi koodista joka yrittäisi esimerkiksi käsitellä virheellistä saraketta tai asettaa sarakkeeseen väärän tyyppistä dataa.
Etuja on siis paljon. Mutta ennen kaikkea nyt ei ole enää riskiä SQL-injektiosta. ORM-kirjasto osaa käsitellä parametrin turvallisesti ja muodostaa pellin alla optimoidut SQL-kyselyt haulle.
Suosittuja ORM-kirjastoja
Tässä on joitain ORM-kirjastoja joita kannattaa kokeilla:
Python:
- SQLAlchemy: https://github.com/sqlalchemy/sqlalchemy
- Django ORM (Django-kirjaston mukana tuleva ORM): https://docs.djangoproject.com/en/5.0/topics/db/queries/
- Peewee: https://github.com/coleifer/peewee
- Pony ORM: https://github.com/ponyorm/pony
Java:
- Hibernate: https://github.com/hibernate/hibernate-orm
- MyBatis: https://github.com/mybatis/mybatis-3
- JOOQ (Java Object Oriented Querying): https://github.com/jOOQ/jOOQ
Ruby:
- ActiveRecord (Rails-kirjaston mukana tuleva ORM): https://guides.rubyonrails.org/active_record_basics.html
- Sequel: https://github.com/jeremyevans/sequel
JavaScript (Node.js):
- Prisma: https://github.com/prisma/prisma
- Sequelize: https://github.com/sequelize/sequelize
- TypeORM: https://github.com/typeorm/typeorm
- Bookshelf.js: https://github.com/bookshelf/bookshelf
C#:
- Entity Framework (EF): https://learn.microsoft.com/en-us/ef/
- Dapper: https://github.com/DapperLib/Dapper
PHP:
- Doctrine ORM: https://github.com/doctrine/orm
- Eloquent (Laravelin mukana tuleva ORM): https://laravel.com/docs/10.x/eloquent
Go:
- GORM: https://github.com/go-gorm/gorm
Toinen vaihtoehto: Parameterisoidut kyselyt
ORM-kirjaston lisäksi on toinenkin tapa rakentaa SQL-kyselyitä turvallisesti - parameterisoidut kyselyt (prepared statements). Mutta tämä lähestymistapa ei sisällä ORM:in muita etuja eikä ole yhtä turvallinenkaan koska joudut aina olemaan hieman varuillasi ettei kiireessä tapahdu lipsahduksia jossa parameterisoituja kyselyitä unohdetaan käytää oikein.
Parameterisoidut kyselyt näyttävät tältä.
def hae_kayttaja_tietokannasta(kayttajanimi):
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (kayttajanimi,))
kayttaja_tiedot = cursor.fetchone()
return kayttaja_tiedot
Tässä esimerkissä käytetään sqlite3-moduulia, ja parametroitu kysely muodostetaan käyttämällä ?-merkkejä paikkojen varaamiseksi myöhempää syöttöä varten. Näin vältetään suora syötteen sisällyttäminen SQL-lausekkeeseen ja siten suojellaan sovellusta SQL-injektioilta. SQL-kirjasto osaa itse korvata kysymysmerkit annettujen parametrien arvoilla turvalisesti.
Kovennuksia
Käydään seuraavaksi läpi pari kovennusta jota voit (hieman tekkistäkistä riippuen) mahdollisesti tehdä havaitaksesi hyökkäyksen ja vähentääksesi siitä aiheutuvaa vahinkoa. Nämä eivät korvaa turvallisien ohjelmointimenetelmien (kuten ORM) käyttöä mutta ovat hyviä lisäsuojia.
WAF-tuote
WAF (Web application firewall) tuotteet ovat yleensä aika hyviä torppaamaan tai ainakin havaitsemaan ja hankaloittamaan SQL-injektiohyökkäyksiä. Sellainen kannattaa siis asettaa Internetin ja sovelluksen väliin tarpeeksi tiukalla säännöstöllä.
Lisäksi WAF-tuotteen havainnot kannattaa kytkeä automaattiseen hälytysjärjestelmään jotta saat reaaliajassa tiedon jo siitä kun joku alkaa etsiä haavoittuvuuksia sovelluksestasi.
False-positiivisia voi vähentää tehokkaasti tekemällä hälytyksiä vain kun haitallinen HTTP-viesti kohdistuu host-headerin perusteella sovellukseen eikä ole vain satunnainen Internetistä tullut satunnaiseen IP-osoitteeseen kohdistunut automaattinen skanni.
Tietokantakäyttäjän oikeuksien, erityisesti metadataan pääsyn, minimointi
SQL-injektion sattuessa hyökkääjä saa pääsyn tietokantaan niillä oikeuksilla joilla sovellus on kyselyn tehnyt. Siksi sovellukselle ei koskaan kannata antaa yhtään ylimääräistä oikeutta tietokantaan.
Kaikki ylimääräiset operaatiot kuten tiedostojen luku-tai kirjoitus, verkkoyhteyksien muodostaminen tai käyttöjärjestelmäkomentojen ajaminen kannattaa tietysti estää.
Kaikista tärkein on kuitenkin metadatan lukemisen estäminen, ja se on valitettavasti monessa tietokannassa helpommin sanottu kun tehty. Tietokantojen suunnittelu ei aina mene tietoturva etummaisena.
Metadatalla tarkoitetaan tietoja kuten "information_schema" taulu josta voi selvittää tietokannan rakenteen (taulut, sarakkeet, jne). Ilman näitä tietoja hyökkääjän voi monesti olla vaikea varastaa juuri ne tiedot jotka hyökkääjä haluaisi.
Tietokantavirheiden valvonta
Jos sovelluksen tuotantoympäristössä tapahtuu SQL-syntaksivirhe, kyse saattaa erittäin todennäköisesti olla SQL-injektiohaavoittuvuudesta joka on joko löytynyt sattumalta käyttäjän syöttäessä ei-odotettuja merkkejä sovellukseen, tai sitten kyse on hyökkääjästä joka on juuri löytänyt haavoittuvuuden ja aikoo seuraavaksi automatisoida sen hyödyntämisen ja imuroida koko sovelluksen tietokannan ulos.
Tällaisten virheiden kytkeminen reaaliaikaiseen tietoturvavalvontaan on siis erittäin suositeltavaa.
Yhteenveto
Summa summarum: Oikea tapa välttää SQL-injektiot on käyttää ORM-kirjastoa jolloin voidaan kokonaan välttää riski siitä että kehittäjät vahingossakaan aiheuttaisivat SQL-injektio -haavoittuvuuksia sovellukseen.
Vaihtoehtona esiteltiin parameterisoidut kyselyt joilla on teknisesti mahdollista rakentaa SQL-kyselyitä turvallisesti, mutta siinä ei saa muita ORM-kirjaston etuja kuten koodissa määriteltyä valmiiksi tyypitettyä tietokantarakennetta. Lisäksi on huomion arvoista että parameterisoitujen kyselyiden käyttämisestä on helppo lipsahtaa raa'an SQL:n rakentamiseen tekstinä, koska loppujen lopuksi parameterisoidut kyselyt ovat edelleen koodissa rakennettua raakaa SQL:ää.
Lopuksi käytiin pari kovennusta jotka saattavat auttaa hyökkäyksen havaitsemisessa ja vahingon minimoinnissa jos haavoittuvuus kuitenkin sovellukseen pääsisi tulemaan.
Valmis ryhtymään eettiseksi hakkeriksi?
Aloita jo tänään.
Hakatemian jäsenenä saat rajoittamattoman pääsyn Hakatemian moduuleihin, harjoituksiin ja työkaluihin, sekä pääset discord-kanavalle jossa voit pyytää apua sekä ohjaajilta että muilta Hakatemian jäseniltä.