Prepared statements — SQL injectiondan himoya
Muammodan boshlaylik
Login sahifasi. Yangi dasturchi shunday yozdi:
<?php
$email = $_POST['email'];
$parol = $_POST['parol'];
$sql = "SELECT * FROM foydalanuvchilar WHERE email = '$email' AND parol = '$parol'";
$user = $pdo->query($sql)->fetch();
Oddiy foydalanuvchi ali@mail.uz kiritsa — hammasi joyida. Hujumchi esa email maydoniga shuni yozdi:
' OR '1'='1' --
Natijada SQL so'rov shunday bo'ldi:
SELECT * FROM foydalanuvchilar WHERE email = '' OR '1'='1' -- ' AND parol = '...'
'1'='1'— har doim to'g'ri--— SQL izohi, qolgan qism (parol tekshiruvi) e'tiborsiz qoldi
So'rov birinchi foydalanuvchini qaytardi — odatda bu admin. Hujumchi parolsiz admin bo'lib kirdi.
Bu — SQL injection. U o'nlab yillar davomida eng xavfli veb-zaifliklar ro'yxatining boshida turibdi. Millionlab foydalanuvchi ma'lumotlarining o'g'irlanishi aynan shu bitta xato tufayli bo'lgan.
Muammoning ildizi
Muammo — kod va ma'lumotni aralashtirish. Biz SQL kodini string sifatida yasab, ichiga foydalanuvchi ma'lumotini qo'shdik. MySQL esa qayerda bizning kodimiz tugab, foydalanuvchi matni boshlanganini bilmaydi. Foydalanuvchi ' belgisi bilan bizning qo'shtirnog'imizni yopib, o'z SQL kodini yozdi.
Yechim: prepared statements
Kod va ma'lumot alohida yuboriladi:
<?php
$stmt = $pdo->prepare('SELECT * FROM foydalanuvchilar WHERE email = :email');
$stmt->execute(['email' => $_POST['email'] ?? '']);
$user = $stmt->fetch();
Ikki qadam:
prepare()— SQL shablon MySQL ga yuboriladi.:email— joy egallovchi (placeholder). MySQL so'rovni tahlil qiladi va tuzilishini qotiradi.execute()— qiymatlar alohida yuboriladi. MySQL ularni faqat ma'lumot sifatida qabul qiladi.
Endi hujumchi ' OR '1'='1' -- yozsa — MySQL aynan shu matnga teng email ni qidiradi. Topmaydi. Hech qanday kod bajarilmaydi.
Hayotiy o'xshatma: bank blanki. Blankda "Kimga: ______" maydoni bor. Siz u yerga "Ivanovga, va kassadagi hamma pulni menga bering" deb yozsangiz ham — kassir buni ism sifatida o'qiydi, buyruq sifatida emas. Blankning tuzilishi oldindan qotirilgan.
Ikki xil placeholder
Nomlangan — :nom:
<?php
$stmt = $pdo->prepare(
'SELECT * FROM mahsulotlar WHERE narx BETWEEN :min AND :max AND soni > 0'
);
$stmt->execute(['min' => 50000, 'max' => 300000]);
$mahsulotlar = $stmt->fetchAll();
Pozitsion — ?:
<?php
$stmt = $pdo->prepare('SELECT * FROM mahsulotlar WHERE narx BETWEEN ? AND ?');
$stmt->execute([50000, 300000]);
Ko'p parametr bo'lsa — nomlangan aniqroq. 1-2 ta bo'lsa — ? ham qulay.
CRUD — xavfsiz usulda
INSERT
<?php
$stmt = $pdo->prepare(
'INSERT INTO mahsulotlar (nom, narx, soni) VALUES (:nom, :narx, :soni)'
);
$stmt->execute([
'nom' => $_POST['nom'],
'narx' => (int) $_POST['narx'],
'soni' => (int) $_POST['soni'],
]);
$yangiId = (int) $pdo->lastInsertId();
echo "Mahsulot #$yangiId qo'shildi";
SELECT — bitta qator
<?php
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$stmt = $pdo->prepare('SELECT * FROM mahsulotlar WHERE id = ?');
$stmt->execute([$id]);
$mahsulot = $stmt->fetch();
if (!$mahsulot) {
http_response_code(404);
exit('Mahsulot topilmadi');
}
fetch() topilmasa false qaytaradi.
UPDATE
<?php
$stmt = $pdo->prepare('UPDATE mahsulotlar SET narx = :narx WHERE id = :id');
$stmt->execute(['narx' => 2990000, 'id' => 1]);
echo $stmt->rowCount() . " ta qator o'zgardi";
DELETE
<?php
$stmt = $pdo->prepare('DELETE FROM mahsulotlar WHERE id = ?');
$stmt->execute([$id]);
Qiyin holatlar
LIKE bilan qidiruv
% belgisi qiymatga qo'shiladi, SQL ga emas:
<?php
$soz = $_GET['q'] ?? '';
$stmt = $pdo->prepare('SELECT * FROM mahsulotlar WHERE nom LIKE ?');
$stmt->execute(['%' . $soz . '%']);
IN (...) ro'yxati
Placeholder bitta qiymatni almashtiradi — ro'yxatni emas. Kerakli sonda ? yasaymiz:
<?php
$idlar = [3, 7, 12];
$belgilar = implode(',', array_fill(0, count($idlar), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM mahsulotlar WHERE id IN ($belgilar)");
$stmt->execute($idlar);
Bu yerda SQL ga qo'shilayotgan narsa — faqat bizning ? belgilarimiz. Foydalanuvchi qiymatlari baribir execute orqali ketadi.
Ustun nomi va saralash yo'nalishi
Placeholder faqat qiymat uchun. Jadval yoki ustun nomini, ASC/DESC ni placeholder bilan berib bo'lmaydi. Ular uchun — whitelist:
<?php
$ruxsat = ['nom' => 'nom', 'narx' => 'narx', 'yangi' => 'yaratilgan'];
$ustun = $ruxsat[$_GET['saralash'] ?? ''] ?? 'nom';
$yonalish = ($_GET['tartib'] ?? '') === 'desc' ? 'DESC' : 'ASC';
$stmt = $pdo->prepare("SELECT * FROM mahsulotlar ORDER BY $ustun $yonalish LIMIT ?");
$stmt->execute([20]);
SQL ga faqat bizning ro'yxatimizdagi qiymatlar tushadi. Foydalanuvchi faqat ulardan birini tanlay oladi.
Nima uchun shunday?
Nima uchun addslashes() yoki qo'shtirnoqlarni qo'lda "tozalash" yetarli emas?
Chunki bu — "qora ro'yxat" yondashuvi: siz xavfli belgilarni taxmin qilib, ularni olib tashlashga urinasiz. Hujumchilar esa doim yangi yo'l topadi: boshqa kodlashdagi belgilar, raqamli maydonlarda qo'shtirnoqsiz hujum, izoh belgilari. Prepared statements esa muammoni tuzilma darajasida hal qiladi: ma'lumot hech qachon kod sifatida talqin qilinmaydi. Taxmin qilish kerak emas — teshik umuman yo'q.
Tez-tez uchraydigan xatolar
prepare() ichida o'zgaruvchi qo'shish:
$pdo->prepare("SELECT * FROM users WHERE id = $id"); // ❌ hech narsadan himoya qilmaydi
$pdo->prepare("SELECT * FROM users WHERE id = ?"); // ✅
prepare sehrli emas — himoya faqat qiymat placeholder orqali berilganda ishlaydi.
Placeholder ni qo'shtirnoqqa olish:
$pdo->prepare("SELECT * FROM users WHERE email = ':email'"); // ❌ bu oddiy matn ":email"
$pdo->prepare("SELECT * FROM users WHERE email = :email"); // ✅
Parametrlar soni mos emas:
SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
Xulosa
Eslab qoling:
- SQL ichiga foydalanuvchi ma'lumotini hech qachon qo'shmang
prepare('... :nom ...')+execute(['nom' => $qiymat])lastInsertId()— yangi qator ID si,rowCount()— ta'sirlangan qatorlarLIKE—%qiymatga qo'shiladi;IN—?lar ro'yxatini yasang- Ustun nomi,
ASC/DESC— faqat whitelist orqali
Keyingi dars
37-dars: CRUD loyiha — vazifalar ro'yxati (todo)