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.
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:
# 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:
python zaif.py
Ekranda shunga o‘xshash natija chiqadi:
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:
print("\nHujum:")
print(login("' OR '1'='1"))
Endi natijaga qarang:
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:
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:
print("\nKartalar:")
print(login("' UNION SELECT id, number, 0 FROM cards --"))
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:
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:
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:
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:
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:
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:
# 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.