Asosiy kontentga o‘tish

Maqolalar 7 daqiqa 76 ko‘rish

SQL injection'ni o‘z ko‘zingiz bilan ko‘ring va yoping

Eng qadimiy zaifliklardan biri, lekin hali ham ishlab turibdi. Kichik zaif ilova yozamiz, uni o‘zimiz buzamiz, keyin bitta o‘zgarish bilan yopamiz — hammasini o‘z kompyuteringizda takrorlab ko‘rasiz.

database security backend

Mundarija

SQL injection haqida o‘qishning eng foydali usuli — uni o‘z qo‘lingiz bilan ishlatib ko‘rish. Xavfsiz muhitda, o‘z kompyuteringizda buni qilish oson va bir marta ko‘rgan odam bu xatoni umr bo‘yi qaytarmaydi. Shuning uchun bu maqolada nazariyani minimal qilib, amaliyotni asosiy qilamiz.

Bizga faqat Python va uning ichida tayyor keladigan sqlite3 kerak, boshqa hech narsa o‘rnatish shart emas. Bo‘sh papka ochib, ishni boshlaymiz.

Zaif ilovani quramiz#

Avval kichkina baza yasab, unga ikkita foydalanuvchi qo‘shamiz. Quyidagi kodni zaif.py nomli faylga saqlang:

Python
# zaif.py
import sqlite3

db = sqlite3.connect(":memory:")  # baza xotirada yashaydi, fayl kerak emas
db.executescript("""
    CREATE TABLE users (id INTEGER, email TEXT, is_admin INTEGER);
    INSERT INTO users VALUES (1, 'olim@misol.uz', 0);
    INSERT INTO users VALUES (2, 'admin@misol.uz', 1);

    -- login bilan hech qanday aloqasi yo'q jadval — keyinroq kerak bo'ladi
    CREATE TABLE cards (id INTEGER, owner TEXT, number TEXT);
    INSERT INTO cards VALUES (1, 'olim@misol.uz', '8600 1234 5678 9012');
    INSERT INTO cards VALUES (2, 'admin@misol.uz', '8600 9999 8888 7777');
""")


def login(email):
    # DIQQAT: bu ataylab xavfli yozilgan — pastda nima uchunligini ko'ramiz
    query = f"SELECT id, email, is_admin FROM users WHERE email = '{email}'"
    print("Yuborilgan so'rov:", query)
    return db.execute(query).fetchall()


print("Oddiy kirish:")
print(login("olim@misol.uz"))

Ishga tushiramiz:

Terminal
python zaif.py

Ekranda shunga o‘xshash natija chiqadi:

Natija
Oddiy kirish:
Yuborilgan so'rov: SELECT id, email, is_admin FROM users WHERE email = 'olim@misol.uz'
[(1, 'olim@misol.uz', 0)]

Hozircha hammasi joyida ishlayapti: email yubordik, o‘sha foydalanuvchi qaytdi. Endi qiziq qismi boshlanadi.

Ilovani buzamiz#

Faylning oxiriga, print qatorlaridan keyin quyidagini qo‘shing va yana ishga tushiring:

Python
print("\nHujum:")
print(login("' OR '1'='1"))

Endi natijaga qarang:

Natija
Hujum:
Yuborilgan so'rov: SELECT id, email, is_admin FROM users WHERE email = '' OR '1'='1'
[(1, 'olim@misol.uz', 0), (2, 'admin@misol.uz', 1)]

Mana shu — SQL injection. Biz email o‘rniga ' OR '1'='1 deb yozgan edik, va bu satr so‘rovni butunlay boshqacha narsaga aylantirib yubordi. "Yuborilgan so'rov" qatoriga diqqat bilan qarang: bizning matnimiz endi so‘rovning kodiga aylanib qolgan. '1'='1' sharti har doim rost bo‘lgani uchun baza barcha foydalanuvchini qaytardi, ular orasida admin ham bor.

Sababi shu bir qatorda:

Python
query = f"SELECT ... WHERE email = '{email}'"

Bu yerda biz foydalanuvchining matnini to‘g‘ridan-to‘g‘ri so‘rovning ichiga yopishtirdik. Baza esa kelgan matnning qaysi qismi bizning kodimiz, qaysi qismi foydalanuvchi ma'lumoti ekanini ajrata olmaydi — u hammasini bitta buyruq deb o‘qiydi.

Bu faqat boshlanishi edi#

Barcha foydalanuvchini ko‘rish yomon, lekin eng yomoni emas. Hujumchi shu teshikdan butunlay boshqa jadvalni ham o‘qiy oladi. Faylning oxiriga yana bitta qator qo‘shing:

Python
print("\nKartalar:")
print(login("' UNION SELECT id, number, 0 FROM cards --"))
Natija
Kartalar:
Yuborilgan so'rov: SELECT id, email, is_admin FROM users WHERE email = '' UNION SELECT id, number, 0 FROM cards --'
[(1, '8600 1234 5678 9012', 0), (2, '8600 9999 8888 7777', 0)]

Diqqat qiling: bu — login funksiyasi. U kartalar jadvaliga umuman murojaat qilmaydi, uning kodida cards degan so‘z yo‘q. Shunga qaramay ekranda karta raqamlari turibdi. UNION SELECT hujumchiga bazadagi istalgan jadvalni shu bitta teshik orqali o‘qish imkonini beradi — aynan shu tarzda haqiqiy loyihalardan butun bazalar o‘g‘irlangan.

"DROP TABLE" ni sinab ko‘rsangiz#

Ko‘pchilik eshitgan klassik hujum — '; DROP TABLE users; --. Uni ham sinab ko‘ring, lekin natija sizni ajablantiradi:

Natija
sqlite3.ProgrammingError: You can only execute one statement at a time.

Jadval joyida qoldi. Sababi sizning kodingizda emas: Python'ning sqlite3 drayveri bitta execute da bittadan ortiq buyruqni bajarishdan bosh tortadi. Ya'ni sizni kodingiz emas, drayver qutqardi — va bu juda ishonchsiz himoya, chunki boshqa bazada u yo‘q.

Buni tekshirib ko‘rdim: aynan o‘sha satrni PostgreSQL va psycopg bilan yozilgan xuddi shunday zaif kodga yuborsangiz, hech qanday xato chiqmaydi va jadval rostdan o‘chib ketadi:

Natija
relation "users" does not exist

Xulosa shu: "menda ishlamadi" degani "men xavfsizman" degani emas.

Endi yopamiz#

Yechim ajablanarli darajada oddiy. login funksiyasidagi so‘rovni shunday o‘zgartiring:

Python
def login(email):
    query = "SELECT id, email, is_admin FROM users WHERE email = ?"
    # email endi so'rovga yopishtirilmaydi, alohida uzatiladi
    return db.execute(query, [email]).fetchall()

Ikki narsa o‘zgardi: so‘rov ichidagi '{email}' o‘rnida endi ? belgisi turibdi, va email execute ga ikkinchi argument sifatida, ro‘yxatda uzatilyapti. Endi yuqoridagi uchala hujumni ham qaytadan sinab ko‘ring:

Natija
Oddiy kirish: [(1, 'olim@misol.uz', 0)]
Hujum: []
Kartalar: []
DROP: []
users jadvali: (2,)

Oddiy kirish ishlayapti, uchala hujum esa bo‘sh ro‘yxat qaytardi. Endi baza ' OR '1'='1 ni kod deb emas, shunchaki qidirilishi kerak bo‘lgan g‘alati email deb qabul qildi. Bunday email hech kimda yo‘q, shuning uchun javob bo‘sh. E'tibor bering: DROP TABLE endi xato ham bermaydi — u shunchaki hech kimga mos kelmaydigan uzun email bo‘lib qoldi, jadval esa joyida. Foydalanuvchi nima yozsa ham, u endi faqat qidiriladigan qiymat bo‘lib qoladi, buyruqqa aylana olmaydi.

Bu usul parametrlangan so‘rov deb ataladi. ? — bu "shu joyga foydalanuvchi bergan qiymat keladi, lekin unga kod sifatida qaramang" degan belgi.

Amalda buni ORM qiladi#

Kundalik ishda siz odatda xom SQL yozmaysiz, Django yoki SQLAlchemy kabi ORM ishlatasiz. Yaxshi xabar shuki, ular parametrlashni o‘zi qiladi:

Python
User.objects.filter(email=email)  # ichida parametrlangan so'rov — xavfsiz

Lekin bitta joyda hushyor bo‘lish kerak. ORM ichida ham xom SQL yozish imkoni bor — Django'da bu raw() va extra(), va aynan o‘sha yerda odamlar eski xatoni qaytaradi:

Python
# YANA XAVFLI — f-string qaytib keldi
User.objects.raw(f"SELECT * FROM users WHERE email = '{email}'")

# TO'G'RISI — bu yerda ham parametr ishlatiladi
User.objects.raw("SELECT * FROM users WHERE email = %s", [email])

Faqat qiymatlarni parametrlab bo‘ladi

Parametr faqat qiymat o‘rnida ishlaydi — email, id, sana. Jadval yoki ustun nomini bu usulda uzatib bo‘lmaydi. Agar sizga foydalanuvchi tanlagan ustun bo‘yicha saralash kerak bo‘lsa, kelgan qiymatni parametr qilmang, balki o‘zingiz ruxsat bergan nomlar ro‘yxati bilan solishtiring: ro‘yxatda bo‘lsa ishlatasiz, bo‘lmasa rad etasiz.

Yodda qoladigan bitta jumla#

Butun mavzuni bitta qoidaga siqish mumkin: foydalanuvchi kiritgan narsa hech qachon so‘rov matniga qo‘shilmasin. Qiymatlar doim alohida, parametr sifatida uzatiladi. Bu qoida yigirma besh yildan beri o‘zgargani yo‘q, va uni bir marta o‘z ko‘zingiz bilan ko‘rgan bo‘lsangiz, endi hech qachon unutmaysiz.

7 daqiqa 76 marta o‘qildi