Prepared statements — SQL injectiondan himoya

~20 daq · PHP · Ma'lumotlar bazasi

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:

  1. prepare() — SQL shablon MySQL ga yuboriladi. :email — joy egallovchi (placeholder). MySQL so'rovni tahlil qiladi va tuzilishini qotiradi.
  2. 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 qatorlar
  • LIKE — % 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)