SQL injekcijos: kaip apsisaugoti nuo atakų

Kas yra SQL injekcija ir kodėl ji vis dar aktuali

SQL injekcija – tai viena seniausių ir kartu vis dar populiariausių kibernetinių atakų rūšių. Nors apie ją kalbama jau daugiau nei du dešimtmečius, ji tebėra OWASP Top 10 sąraše ir kasmet sukelia milijoninių nuostolių įmonėms visame pasaulyje. Esmė paprasta: piktavalis įterpia kenkėjišką SQL kodą į jūsų aplikacijos įvesties laukus, tikėdamasis, kad sistema jį vykdys kaip legitimią komandą.

Problema ta, kad daugelis programuotojų vis dar mano, jog tai kažkas, kas nutinka tik pradedantiesiems ar seniems projektams. Realybė kitokia – net didžiosios kompanijos reguliariai patiria SQL injekcijų atakas. 2023 metais MOVEit duomenų bazės pažeidimas, kuris paveikė šimtus organizacijų, įskaitant vyriausybines įstaigas, buvo susijęs būtent su SQL injekcija.

Kodėl tai vis dar vyksta? Pirma, legacy kodas. Antra, laiko spaudimas projektuose. Trečia, nepakankamas saugumo supratimas tarp kūrėjų. Ketvirtą, naujų programuotojų trūkumas patirties. Ir galiausiai – per didelis pasitikėjimas framework’ais, kurie neva „viską padaro už tave”.

Kaip veikia SQL injekcijos praktikoje

Įsivaizduokite paprastą prisijungimo formą. Vartotojas įveda savo el. paštą ir slaptažodį. Jūsų kodas gali atrodyti maždaug taip:


query = "SELECT * FROM users WHERE email = '" + userEmail + "' AND password = '" + userPassword + "'"

Atrodo nekaltas, tiesa? Bet kas nutiks, jei vartotojas vietoj normalaus el. pašto įves: ' OR '1'='1' --?

Jūsų užklausa taps tokia:


SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = ''

Dviejų brūkšnių simboliai SQL kalboje reiškia komentarą, todėl visa, kas po jų, bus ignoruojama. O sąlyga '1'='1' visada teisinga, todėl užklausa grąžins visus vartotojus iš duomenų bazės. Piktavalis gali prisijungti kaip pirmasis vartotojas sistemoje – dažniausiai tai administratorius.

Bet tai tik paprasčiausias pavyzdys. Sudėtingesnės atakos gali:

  • Ištrinti visą duomenų bazę naudojant DROP TABLE komandas
  • Įterpti naujus duomenis, pavyzdžiui, sukurti administratoriaus paskyrą
  • Nuskaityti visą duomenų bazės struktūrą ir turinį
  • Vykdyti operacinės sistemos komandas (kai DB vartotojas turi per daug teisių)
  • Atlikti DoS atakas prieš serverį

Yra net automatizuotų įrankių kaip SQLmap, kurie gali automatiškai aptikti ir išnaudoti SQL injekcijos pažeidžiamumus. Tai reiškia, kad net nepatyrę užpuolikai gali atakuoti jūsų sistemą.

Prepared statements – jūsų pirmoji gynybos linija

Svarbiausia ir efektyviausia apsauga nuo SQL injekcijų yra parametrizuotos užklausos, dar vadinamos prepared statements. Tai mechanizmas, kuris atskiria SQL kodą nuo duomenų. Duomenų bazė iš anksto sukompiliuoja užklausos struktūrą, o tada atskirai apdoroja parametrus kaip duomenis, o ne kaip vykdytiną kodą.

Štai kaip tai atrodo PHP su PDO:


$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND password = :password');
$stmt->execute(['email' => $userEmail, 'password' => $userPassword]);

Python su psycopg2:


cursor.execute("SELECT * FROM users WHERE email = %s AND password = %s", (user_email, user_password))

Node.js su pg biblioteka:


client.query('SELECT * FROM users WHERE email = $1 AND password = $2', [userEmail, userPassword])

Esmė ta, kad net jei piktavalis įterps kenkėjišką kodą, jis bus traktuojamas kaip paprastas tekstas, o ne SQL komanda. Duomenų bazė ieškos vartotojo, kurio el. paštas tikrai yra ' OR '1'='1' --, o tokio, žinoma, neegzistuoja.

Svarbu suprasti: prepared statements turi būti naudojami VISUR, kur vartotojo įvestis patenka į SQL užklausą. Tai apima ne tik akivaizdžius atvejus kaip prisijungimo formos, bet ir:

  • Paieškos laukus
  • Filtravimo parametrus URL
  • Cookie reikšmes
  • HTTP antraštes
  • Bet kokius kitus duomenis, kurie gali būti manipuliuojami vartotojo

ORM ir framework’ai – ar jie tikrai saugūs

Daugelis šiuolaikinių framework’ų ir ORM (Object-Relational Mapping) bibliotekų automatiškai naudoja parametrizuotas užklausos. Django ORM, Laravel Eloquent, Entity Framework, Hibernate – visi jie pagal nutylėjimą apsaugo nuo SQL injekcijų, jei naudojate juos teisingai.

Bet štai problema – „jei naudojate teisingai”. Dažnai programuotojai mano, kad naudodami ORM jie yra visiškai apsaugoti, ir tai sukelia netikėtų pažeidžiamumų.

Pavyzdžiui, Django:


# Saugu
User.objects.filter(email=user_email)

# NESAUGU!
User.objects.raw('SELECT * FROM users WHERE email = "' + user_email + '"')

Arba Laravel:


// Saugu
DB::table('users')->where('email', $userEmail)->get();

// NESAUGU!
DB::select('SELECT * FROM users WHERE email = "' . $userEmail . '"');

Matote tendenciją? Kai tik pradėti rašyti „raw” SQL užklausas arba naudoti string concatenation, jūs pradedate rizikuoti. Ir dažnai tai daroma dėl „optimizacijos” ar „sudėtingų užklausų”, kurias ORM tariamai negali padaryti.

Kitas pavojingas dalykas – dinamiškai generuojamos užklausos su stulpelių ar lentelių vardais. Prepared statements neveikia su identifikatoriais (lentelių, stulpelių vardais), todėl čia reikia kitokio požiūrio – whitelist validacijos.


// NESAUGU
$orderBy = $_GET['sort'];
$query = "SELECT * FROM products ORDER BY " . $orderBy;

// Saugu
$allowedColumns = ['name', 'price', 'date'];
$orderBy = in_array($_GET['sort'], $allowedColumns) ? $_GET['sort'] : 'name';
$query = "SELECT * FROM products ORDER BY " . $orderBy;

Įvesties validacija – antroji gynybos linija

Nors prepared statements yra pagrindinė apsauga, įvesties validacija yra svarbus papildomas sluoksnis. Principas „niekada nepasitikėk vartotojo įvestimi” turėtų būti įrašytas auksinėmis raidėmis virš kiekvieno programuotojo darbo stalo.

Validacija turėtų vykti keliuose lygmenyse:

Kliento pusėje (frontend) – tai pagerina vartotojo patirtį, bet NIEKADA neturėtų būti laikoma saugumo priemone. JavaScript validaciją galima lengvai apeiti.

Serverio pusėje (backend) – čia vyksta tikroji validacija. Patikrinkite:

  • Duomenų tipą (ar tai tikrai skaičius, data, el. paštas?)
  • Ilgį (ar neviršija maksimalaus leistino)
  • Formatą (ar atitinka laukiamą šabloną)
  • Reikšmių diapazoną (ar patenka į leistinus limitus)

Pavyzdžiui, jei laukiate vartotojo ID, kuris turėtų būti teigiamas sveikasis skaičius:


$userId = filter_var($_GET['id'], FILTER_VALIDATE_INT);
if ($userId === false || $userId < 1) { die('Invalid user ID'); }

Bet atsargiai su "sanitization" funkcijomis, kurios bando "išvalyti" SQL specialius simbolius. Funkcijos kaip mysql_real_escape_string() ar panašios yra pasenusios ir nepatikimos. Jos gali būti apeinamos naudojant skirtingas koduotes ar kitus triukus. Geriau naudoti prepared statements ir whitelist validaciją.

Mažiausių privilegijų principas duomenų bazėje

Vienas dažnai ignoruojamas saugumo aspektas – duomenų bazės vartotojo teisės. Daugelis aplikacijų jungiasi prie DB su root ar administratoriaus teisėmis. Tai katastrofiška klaida.

Jei jūsų web aplikacija naudoja DB vartotoją su visomis teisėmis, sėkminga SQL injekcija gali:

  • Ištrinti visas duomenų bazes serveryje
  • Skaityti sisteminius failus
  • Rašyti failus į serverio failų sistemą
  • Vykdyti operacinės sistemos komandas

Teisingas požiūris:

Sukurkite atskirą DB vartotoją kiekvienai aplikacijai su minimaliomis būtinomis teisėmis. Jei jūsų aplikacija tik skaito ir rašo duomenis į konkrečias lenteles, ji neturėtų turėti teisių DROP, CREATE ar ALTER.


-- Sukurti vartotoją
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';

-- Suteikti tik būtinas teises
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'webapp'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.posts TO 'webapp'@'localhost';

-- Atnaujinti privilegijas
FLUSH PRIVILEGES;

Skirkite read-only vartotojus dalims aplikacijos, kurios tik skaito duomenis (pavyzdžiui, reporting sistemoms).

Niekada nenaudokite root vartotojo aplikacijose. Rimtai, niekada.

Išjunkite nebūtinas DB funkcijas kaip LOAD_FILE(), INTO OUTFILE, jei jų nenaudojate.

Web Application Firewall ir papildomi apsaugos sluoksniai

Nors programinio kodo saugumas yra svarbiausias, papildomi apsaugos sluoksniai gali sustabdyti atakas, kurios praėjo pro pirminę gynybą.

Web Application Firewall (WAF) gali aptikti ir blokuoti įtartinus užklausų šablonus. Populiarūs sprendimai:

  • ModSecurity (open source, integruojasi su Apache/Nginx)
  • Cloudflare WAF (cloud-based)
  • AWS WAF (jei naudojate AWS)
  • Imperva, F5 (komerciniai sprendimai)

WAF veikia analizuodamas HTTP užklausas ir ieškodamas žinomų atakų šablonų. Pavyzdžiui, jei užklausoje aptinka UNION SELECT, DROP TABLE ar kitus įtartinus SQL fragmentus, ji gali užblokuoti užklausą.

Bet atsargiai – WAF nėra stebuklingas sprendimas. Jis gali duoti false positives (blokuoti legitimias užklausas) ir false negatives (praleisti atakas). Tai turėtų būti papildoma apsauga, o ne pagrindinis saugumo mechanizmas.

Intrusion Detection Systems (IDS) gali stebėti duomenų bazės užklausas ir įspėti apie įtartinę veiklą. Pavyzdžiui, jei staiga pradedamos vykdyti neįprastos užklausos ar bandoma pasiekti lenteles, kurios paprastai neprieigos.

Rate limiting gali sulėtinti automatizuotas atakas. Jei kažkas bando SQL injekciją automatizuotais įrankiais, jie darys šimtus ar tūkstančius užklausų per minutę. Rate limiting gali tai aptikti ir laikinai užblokuoti IP adresą.

Logging ir monitoring – registruokite visas DB klaidas ir įtartinas užklausas. Jei pradeda rodytis daug SQL sintaksės klaidų, tai gali būti SQL injekcijos bandymo požymis.

Testavimas ir pažeidžiamumų skenavimas

Kaip žinoti, ar jūsų aplikacija saugi? Vienintelis būdas – testuoti.

Rankinis testavimas – patys pabandykite įterpti paprastus SQL injekcijos payload'us į visus įvesties laukus. Pradėkite nuo paprastų:

  • ' (vienguba kabutė)
  • ' OR '1'='1
  • ' OR '1'='1' --
  • ' UNION SELECT NULL--

Jei matote SQL klaidas ar neįprastą elgesį, turite problemą.

Automatizuotas skenavimas – naudokite specializuotus įrankius:

SQLmap – galingiausias open source SQL injekcijos testavimo įrankis. Gali automatiškai aptikti ir išnaudoti SQL injekcijas:


sqlmap -u "http://example.com/page?id=1" --batch --banner

Burp Suite – profesionalus web aplikacijų saugumo testavimo įrankis su SQL injekcijos aptikimo galimybėmis.

OWASP ZAP – nemokamas alternatyvus Burp Suite, puikus pradedantiesiems.

Acunetix, Netsparker – komerciniai sprendimai su automatizuotu skenavimo ir reporting funkcijomis.

Code review – reguliariai peržiūrėkite kodą ieškodami nesaugių SQL užklausų. Naudokite static analysis įrankius:

  • SonarQube – palaiko daugelį kalbų, aptinka saugumo pažeidžiamumus
  • Bandit (Python), Brakeman (Ruby), ESLint (JavaScript) – kalbai specifiniai įrankiai

Penetration testing – periodiškai samdomkite profesionalius saugumo specialistus, kad atliktų pilną aplikacijos auditą. Jie gali rasti pažeidžiamumus, kurių automatizuoti įrankiai nepastebėjo.

Ką daryti, jei jau nukentėjote nuo atakos

Jei įtariate ar žinote, kad jūsų sistema buvo atakuota SQL injekcija, veiksmai turi būti greiti ir metodiški.

Nedelsiant:

  • Atjunkite pažeistą sistemą nuo interneto (jei įmanoma be didelio verslo sutrikdymo)
  • Pakeiskite visus DB slaptažodžius
  • Peržiūrėkite DB logus ir web serverio logus, ieškodami atakos pėdsakų
  • Identifikuokite pažeidžiamumą ir nedelsiant jį pataisykite

Trumpalaikėje perspektyvoje:

  • Įvertinkite žalą – kokie duomenys galėjo būti pavogti ar pakeisti
  • Patikrinkite duomenų bazės vientisumą – ar nebuvo ištrinti ar pakeisti duomenys
  • Ieškokite backdoor'ų – ar užpuolikas nesukūrė administratoriaus paskyrų ar kitų būdų grįžti
  • Atkurkite duomenis iš backup'ų, jei reikia

Ilgalaikėje perspektyvoje:

  • Atlikite pilną saugumo auditą visos aplikacijos
  • Įdiekite monitoring ir alerting sistemas
  • Apmokysite komandą apie saugų kodavimą
  • Įtraukite saugumo testavimą į CI/CD pipeline
  • Jei buvo pažeisti asmens duomenys, informuokite atitinkamas institucijas pagal GDPR reikalavimus

Svarbu dokumentuoti visą incidentą – kas nutiko, kada, kaip sureagavote, kokios pamokos išmoktos. Tai padės ateityje ir gali būti reikalinga reguliatoriams.

Saugumas kaip kultūra, o ne vienkartin įdiegta funkcija

SQL injekcijos yra išvengiamos problema. Technologijos ir metodai jų prevencijai egzistuoja jau dešimtmečius. Tačiau jos tebėra viena populiariausių atakų rūšių, nes saugumas dažnai traktuojamas kaip kažkas, ką galima pridėti vėliau, o ne kaip integrali kūrimo proceso dalis.

Realybė tokia, kad nėra vieno "sidabrinio kulkos" sprendimo. Prepared statements yra esminiai, bet nepakanka tiesiog jų naudoti – reikia naudoti teisingai ir nuosekliai. ORM padeda, bet negali aklai pasitikėti. WAF prideda apsaugos sluoksnį, bet negali pakeisti saugaus kodo. Testavimas būtinas, bet turi būti reguliarus, o ne vienkartinis.

Geriausias požiūris – "defense in depth": keletas apsaugos sluoksnių, kur kiekvienas papildo kitą. Kai vienas mechanizmas suklumpa, kitas vis tiek gali sustabdyti ataką. Ir svarbiausia – saugumo kultūra komandoje, kur kiekvienas programuotojas supranta rizikas ir žino, kaip rašyti saugų kodą.

Pradėkite nuo pagrindų: visur naudokite prepared statements, validuokite įvestį, taikykite mažiausių privilegijų principą. Tada pridėkite papildomus sluoksnius: WAF, monitoring, reguliarų testavimą. Ir nepamirškite – saugumas nėra vienkartinis projektas, tai nuolatinis procesas. Nauji pažeidžiamumai atsiranda, kodas keičiasi, komanda auga. Reguliarus mokymas, code review ir saugumo auditas turi tapti įprastu darbo ritmu, o ne išimtimi.

Daugiau

Python generators: efektyvi atmintis su yield