PostgreSQL — CRUD, parametrli so'rovlar, SQL Injection himoyasi

~25 daq · Express · Ma'lumotlar Bazasi

SQL Injection — eng xavfli hujum

Avval eng muhim narsadan boshlaymiz, chunki bu xavfsizlik masalasi.

Tasavvur qiling: siz foydalanuvchini ID bo'yicha qidirasiz. Shoshib qolsangiz, shunday yozib qo'yishingiz mumkin:

const id = req.params.id;
const result = await pool.query(`SELECT * FROM users WHERE id = ${id}`);

Bu ishlaydi. Lekin hacker id o'rniga quyidagini yuborsa:

1 OR 1=1

SQL so'rov bunday bo'ladi:

SELECT * FROM users WHERE id = 1 OR 1=1

1=1 har doim to'g'ri — barcha foydalanuvchilar qaytariladi. Yoki yanada xavflisi:

1; DROP TABLE users; --
SELECT * FROM users WHERE id = 1; DROP TABLE users; --

Butun jadval o'chiriladi. Bu — SQL Injection.


Himoya — parametrli so'rovlar

Yechim oddiy: foydalanuvchi kiritmasini SQL ga to'g'ridan-to'g'ri qo'shmaslik. O'rniga $1, $2 placeholder ishlatiladi, qiymatlar alohida beriladi:

// Xavfli:
pool.query(`SELECT * FROM users WHERE id = ${id}`);

// Xavfsiz:
pool.query('SELECT * FROM users WHERE id = $1', [id]);

pg kutubxonasi $1 ni qiymat bilan almashtiradi, lekin avval uni escape qiladi — maxsus belgilar zararsizlantiriladi. Hacker nima yubbormasin — SQL kodi sifatida bajarilmaydi.

Bu qoida — har doim, istisnossiz. Foydalanuvchidan kelgan har qanday ma'lumot parametr sifatida beriladi.


CREATE — yangi qator qo'shish

router.post('/', async (req, res, next) => {
  try {
    const { name, email, age } = req.body;

    const result = await pool.query(
      'INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING *',
      [name, email, age]
    );

    res.status(201).json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});

RETURNING * — qo'shilgan qatorni qaytaradi. Bu muhim: ma'lumotlar bazasi id, created_at kabi avtomatik qiymatlarni o'zi belgilaydi. RETURNING * siz ularni alohida so'ramasdan darhol olasiz.


READ — o'qish

// Hammasi
router.get('/', async (req, res, next) => {
  try {
    const result = await pool.query(
      'SELECT id, name, email FROM users ORDER BY id'
    );
    res.json(result.rows);
  } catch (err) {
    next(err);
  }
});

// Bittasi
router.get('/:id', async (req, res, next) => {
  try {
    const result = await pool.query(
      'SELECT id, name, email FROM users WHERE id = $1',
      [req.params.id]
    );

    if (result.rows.length === 0) {
      return res.status(404).json({ message: 'Topilmadi' });
    }

    res.json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});

result.rows.length === 0 — foydalanuvchi topilmadimi tekshirish. pg xato chiqarmaydi — bo'sh massiv qaytaradi.


UPDATE — yangilash

router.patch('/:id', async (req, res, next) => {
  try {
    const { name, email } = req.body;

    const result = await pool.query(
      'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
      [name, email, req.params.id]
    );

    if (result.rows.length === 0) {
      return res.status(404).json({ message: 'Topilmadi' });
    }

    res.json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});

RETURNING * bu yerda ham muhim — yangilangan qatorni qaytaradi. Aks holda foydalanuvchiga eski ma'lumotni qaytarishga majbur bo'lasiz.


DELETE — o'chirish

router.delete('/:id', async (req, res, next) => {
  try {
    const result = await pool.query(
      'DELETE FROM users WHERE id = $1 RETURNING id',
      [req.params.id]
    );

    if (result.rows.length === 0) {
      return res.status(404).json({ message: 'Topilmadi' });
    }

    res.status(204).send(); // 204 — muvaffaqiyatli, kontent yo'q
  } catch (err) {
    next(err);
  }
});

204 No Content — o'chirishda standart status. Javobda kontent bo'lmaydi.


rowCount — nechta qator ta'sirlandi

result.rows.length o'rniga result.rowCount ham ishlatiladi — ayniqsa UPDATE va DELETE da:

const result = await pool.query(
  'DELETE FROM users WHERE id = $1',
  [req.params.id]
);

if (result.rowCount === 0) {
  return res.status(404).json({ message: 'Topilmadi' });
}

rowCount — nechta qator o'zgarganini ko'rsatadi. 0 bo'lsa — hech narsa topilmadi.


Unique constraint xatosi

Email takrorlanmasligi kerak bo'lsa — ma'lumotlar bazasida UNIQUE constraint qo'yiladi. Takrorlansa — pg xato chiqaradi. Bu xatoni ushlab, foydalanuvchiga tushunarli xabar berish kerak:

router.post('/', async (req, res, next) => {
  try {
    const result = await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [req.body.name, req.body.email]
    );
    res.status(201).json(result.rows[0]);
  } catch (err) {
    // PostgreSQL unique violation kodi: 23505
    if (err.code === '23505') {
      return res.status(409).json({ message: 'Bu email allaqachon mavjud' });
    }
    next(err);
  }
});

err.code === '23505' — PostgreSQL ning unique constraint xatosi kodi. Boshqa PostgreSQL xato kodlari ham bor — ularni bilish kerak bo'lganda PostgreSQL dokumentatsiyasidan topiladi.


Tez-tez uchraydigan xatolar

String interpolatsiya bilan SQL yozish:

// Hech qachon bunday qilmang:
pool.query(`SELECT * FROM users WHERE email = '${email}'`);

Foydalanuvchidan kelgan har qanday ma'lumot — $1, $2 orqali.

rows[0] ni tekshirmaslik:

const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0].name; // rows[0] undefined bo'lsa — TypeError!

Avval rows.length === 0 tekshiriladi, keyin rows[0] ishlatiladi.

Parolni ochiq saqlash:

// Xato — parol ochiq:
INSERT INTO users (email, password) VALUES ($1, $2)
// password = 'qwerty123'

// To'g'ri — hash qilib saqlash (keyingi darslarda):
// password = '$2b$10$...'

Xulosa

CRUD — Create, Read, Update, Delete. Har biri uchun SQL: INSERT, SELECT, UPDATE, DELETE.

RETURNING * — o'zgartirilgan qatorni qaytaradi. rowCount — nechta qator ta'sirlandi.

SQL Injection — eng xavfli hujum. Himoya — parametrli so'rovlar ($1, $2). Bu qoida istisnossiz.


Keyingi dars

15-dars: MySQL — mysql2 moduli, CRUD