IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL42/64-dars20 daqiqa
Mundarija (30)

SQL injection va parametrli so'rovlar: $1, whitelist va dinamik WHERE

Qisqacha: SQL injection — foydalanuvchi kiritgan matn SQL kodining bir qismiga aylanib qolishi. Sabab doim bitta: qiymat SQL satriga + yoki ${} bilan yopishtirilgan. Davosi ham bitta: qiymatni parametr ($1, $2) bilan alohida yuborish — Postgres uni hech qachon kod deb o'qimaydi. Kanon tajribasida yopishtirilgan qidiruv so'zi bilan boshqa foydalanuvchilarning 48 ta vazifasi va 8 ta emaili o'g'irlandi, izohlar jadvali o'chirildi; $1 bilan — 0 qator. Ustun yoki jadval nomi parametr bo'la olmaydi — u uchun oq ro'yxat (whitelist).

Bu darsda

  • SQL injection qanday ishlashini o'z ko'zingiz bilan ko'rasiz (kanon tajribasi).
  • Har foydalanuvchi qiymatini $1, $2 bilan yuborasiz va nega bu himoya qilishini tushuntirasiz.
  • LIKE dagi % va _ ni escape qilasiz.
  • Ustun/jadval nomini oq ro'yxat yoki pg-format %I bilan xavfsiz qo'yasiz.
  • Ro'yxatni = ANY($1) bilan, ixtiyoriy filtrlarni dinamik WHERE bilan yig'asiz.

Oldin bilishingiz kerak: node-postgres (pg) asoslari, SELECT va WHERE, Tagged template literals.

1. Nega bu kerak?

Oldingi darsda har so'rovda $1 yozdik va "nega — keyingi darsda" dedik. Mana o'sha dars.

Tasavvur qiling: 25-qismdagi API'da qidiruv bor — GET /vazifalar?q=bozor. Dasturchi shoshib yozdi:

js
// ❌ HECH QACHON shunday yozmang
const sql = `SELECT id, matn FROM vazifalar
  WHERE egasi_id = 2 AND matn ILIKE '%${q}%'`;

q = "bozor" bo'lsa, hammasi joyida: Dilnozaning (2-foydalanuvchi) bitta vazifasi topiladi. Lekin q ni foydalanuvchi yozadi. U qo'shtirnoq yozsa nima bo'ladi? Qo'shtirnoq SQL satrini yopadi — va qolgan hamma narsa SQL kodi bo'lib qoladi.

O'xshatish: bank xodimiga qog'oz berasiz: "Pulni ___ hisobiga o'tkazing", bo'sh joyni mijoz to'ldiradi. Mijoz yozdi: "Ali hisobiga. Shuningdek, seyfni oching". Xodim qog'ozni boshidan oxirigacha buyruq deb o'qisa — seyf ochiladi. To'g'ri tartib: mijoz yozgan narsa faqat "hisob nomi" maydoni, hech qachon buyruq emas. Parametr — aynan shu maydon.

2. Hujumni ko'ramiz

2.1 Uch hujum — uch natija

Kanon tajribasi (natijalar/tajriba/t42-injection.txt, PostgreSQL 18.6, pg 8.23.1) yuqoridagi xato qidir(soz) funksiyasiga uch xil "qidiruv so'zi" berdi:

text
oddiy qidiruv 'bozor': 1 qator [{"id":"16","matn":"Bozordan meva olish #bozor"}]
injection: boshqalarning vazifalari: 48 qator [{"id":"8","matn":"Bozordan kartoshka va piyoz olish #bozor"},…]
injection: UNION bilan emaillar: 8 qator [{"id":"3","matn":"sardor@example.com"},…]
injection: ; DELETE (simple protocol): 3 natija (oxirgisi 0 qator)
  izohlar qoldi: 0

(Uzun qatorlar … bilan qisqartirildi.)

1-hujum. Kiritilgan so'z: ' OR egasi_id <> 2 OR matn ILIKE '. Yopishtirilgandan keyin SQL shunday bo'ladi:

sql
SELECT id, matn FROM vazifalar
WHERE egasi_id = 2 AND matn ILIKE '%'
  OR egasi_id <> 2 OR matn ILIKE '%'

Birinchi ' foydalanuvchi satrini yopdi, keyin OR egasi_id <> 2 — kod. Natija: 1 qator o'rniga 48 qator — bazadagi hamma vazifa, boshqa odamlarniki ham.

2-hujum. zzz' UNION SELECT id, email FROM foydalanuvchilar --. UNION (To'plam amallari) ikkinchi so'rov natijasini qo'shadi, -- esa qolgan qismni izohga aylantiradi. Javobda 8 ta email — hamma foydalanuvchilar. Haqiqiy ilovada bu parol hash'lari bo'lishi mumkin edi.

3-hujum. x'; DELETE FROM izohlar; SELECT 1 WHERE '' = '. Nuqtali vergul birinchi buyruqni tugatadi va yangisini boshlaydi. Natija: izohlar qoldi: 0 — hamma izohlar o'chdi. (Kanon buni tranzaksiya ichida qildi va keyin ROLLBACK bilan qaytardi — Tranzaksiya asoslari.)

2.2 Nega uchinchi hujum ishladi: ikki protokol

Postgres'ga so'rov yuborishning ikki yo'li bor:

  • Simple query (oddiy) — SQL matni bir xabarda, ichida ; bilan bir nechta buyruq bo'lishi mumkin. pg buni qiymatlar massivi bo'lmaganda ishlatadi.
  • Extended query (kengaytirilgan) — uch bosqich: Parse (SQL shabloni, $1 bilan), Bind (qiymatlar alohida), Execute. Faqat bitta buyruq. pg buni query(sql, [qiymatlar]) da ishlatadi.
sequenceDiagram
  participant N as pg
  participant P as Postgres
  Note over N,P: Simple: matn ichida hammasi
  N->>P: Query: SELECT …'x' va DELETE … (bitta matn)
  Note over N,P: Extended: kod va qiymat alohida
  N->>P: Parse "SELECT … ILIKE $1"
  N->>P: Bind: qiymat x' va DELETE … (faqat matn)
  N->>P: Execute

Diagrammaga qarang: extended'da qiymat Bind xabarida — SQL shablonidan alohida konvertda keladi. Postgres shablonni allaqachon tahlil qilib bo'lgan; qiymat ichidagi ' va ; endi shunchaki harflar.

Kanon buni ham tekshirdi: parametrli so'rovga ikki buyruq berildi.

text
parametr bilan ikki buyruq: XATO 42601 cannot insert multiple commands into a prepared statement

"Tayyorlangan so'rovga bir nechta buyruq qo'yib bo'lmaydi" — extended protokol buni umuman qabul qilmaydi.

Tekshirib ko'ring: Hujumchi nega so'zining oxiriga matn ILIKE ' qo'shdi?

Javob

Dasturchining shabloni oxirida %' bor edi — ochiq qolgan qo'shtirnoqni yopish uchun. Hujumchi o'z qismini shunday tugatdiki, shablonning qolgan %' i to'g'ri SQL bo'lib yopildi (matn ILIKE '%'). Aks holda sintaksis xatosi chiqardi va hujum ishlamasdi. Injection — "shablonni sindirmasdan o'z kodini qo'shish" san'ati.

3. Himoya: parametr ($1)

3.1 To'g'ri qidiruv

js
const xavfsiz = (soz) =>
  client.query(
    "SELECT id, matn FROM vazifalar " +
      "WHERE egasi_id = 2 AND matn ILIKE '%' || $1 || '%'",
    [soz],
  );

'%' || $1 || '%' — % belgilarini SQL'ning o'zi || (satr ulash) bilan qo'shadi; $1 ning qiymati esa alohida keladi. Kanon chiqishi — o'sha birinchi hujum so'zi bilan:

text
parametr: ' OR egasi_id <> 2 ...: 0 qator []

0 qator: Postgres ' OR egasi_id <> 2 OR matn ILIKE ' degan matnni qidirdi. Bunday vazifa yo'q — tamom.

3.2 Parametr qayerda ishlaydi, qayerda yo'q

Parametr — faqat qiymat o'rnida: WHERE x = $1, VALUES ($1, $2), LIMIT $1, SET matn = $1. U SQL tuzilmasi o'rnida ishlamaydi: jadval nomi, ustun nomi, ASC/DESC, kalit so'zlar. Kanon ikkalasini sinadi:

text
ORDER BY $1 (nom emas — doimiy qiymat): 3 qator [{"id":"8"},{"id":"2"},{"id":"1"}]
jadval nomi $1: XATO 42601 syntax error at or near "$1"

Birinchisi ayyorroq: xato yo'q, lekin ORDER BY $1 ustun bo'yicha emas, 'matn' degan doimiy satr bo'yicha saralaydi — hamma qatorda bir xil, ya'ni umuman saralamaydi. Dastur "ishlayapti" ko'rinadi, natija esa tasodifiy tartibda. Ikkinchisi — ochiq sintaksis xatosi: jadval nomi o'rnida qiymat bo'lishi mumkin emas.

3.3 LIKE dagi joker belgilar

Parametr injection'dan himoya qiladi, lekin LIKE/ILIKE ning o'z maxsus belgilari bor: % — istalgan matn, _ — bitta belgi. Ular parametr ichida ham ishlaydi:

text
parametr '%' — hammasini topadi: 6 qator [...]
escape qilingan '%': 0 qator []

Foydalanuvchi % qidirsa — Dilnozaning hamma 6 ta vazifasi chiqdi. Bu xavfsizlik teshigi emas (faqat o'zining vazifalari), lekin noto'g'ri natija; katta jadvalda esa og'ir so'rov. Davosi — \ bilan escape:

js
const likeEscape = (s) => s.replace(/[\\%_]/g, (m) => "\\" + m);

console.log(likeEscape("100%")); // 100\%
console.log(likeEscape("a_b")); // a\_b
console.log(likeEscape("bozor")); // bozor

Postgres'da LIKE ning sukut escape belgisi — \. Avval \ ning o'zini ikkilaymiz (regex'dagi \\), keyin % va _.

Tekshirib ko'ring: Parametr bilan yozilgan WHERE email = $1 ga foydalanuvchi % yubordi. Nechta qator chiqadi?

Javob

Hech qaysi (agar emaili aynan % bo'lgan odam bo'lmasa). = — aniq tenglik, % bu yerda joker emas. Joker faqat LIKE/ILIKE da ishlaydi — escape ham faqat o'sha joyda kerak.

4. Ustun va jadval nomlari: oq ro'yxat

4.1 Oq ro'yxat (whitelist)

API'da ?saralash=yangi bor. Ustun nomini parametr bilan berib bo'lmaydi, yopishtirish — injection. Yechim: foydalanuvchi kalit tanlaydi, SQL bo'lagini esa biz yozamiz.

js
const SARALASH = {
  yangi: "yaratilgan_vaqt DESC, id DESC",
  eski: "yaratilgan_vaqt, id",
  alifbo: "lower(matn), id",
};
const saralash = (kalit) => {
  const sql = SARALASH[kalit];
  if (sql === undefined) {
    throw new Error(`noma'lum saralash: ${kalit}`);
  }
  return client.query(
    `SELECT id, matn FROM vazifalar
     WHERE egasi_id = $1 ORDER BY ${sql} LIMIT 3`,
    [1],
  );
};

Kanon chiqishi:

text
whitelist 'alifbo': 3 qator [{"id":"43","matn":"Blog uchun mavzu tanlash"},…]
whitelist 'id; DROP TABLE x': XATO  noma'lum saralash: id; DROP TABLE x

${sql} yopishtirilgan — lekin u foydalanuvchidan emas, bizning SARALASH obyektimizdan keladi. Foydalanuvchi faqat uchta kalitdan birini tanlay oladi; boshqa har narsa — xato. 25-qismda bu tekshiruv Zod'ning z.enum(["yangi", "eski", "alifbo"]) bilan kirishda qilinadi; obyekt esa ikkinchi himoya.

Diqqat: SARALASH[kalit] da kalit = "constructor" yoki "toString" bo'lsa, oddiy obyekt prototipdan funksiya qaytaradi — undefined emas! Kanon tajribasidagi kod buni tekshirmagan edi; API'ga ko'chirishda tuzatildi («Node qadami» bo'limi). Ishonchli variant: Object.hasOwn(SARALASH, kalit) yoki new Map(...), yoki obyektni Object.create(null) bilan yaratish.

4.2 pg-format va escape funksiyalari

Ba'zan nom oldindan ma'lum emas (masalan admin paneli istalgan jadvalni ko'rsatadi). Unda nomni identifikator sifatida escape qilish kerak. pg-format paketi (kanon: 1.0.4):

  • %I — identifikator (jadval, ustun): qo'shtirnoq "…" ichiga oladi, ichidagi " ni ikkilaydi.
  • %L — literal (qiymat): '…' ichiga, ' ni ikkilaydi.

Kanon chiqishi:

text
pg-format: SELECT matn FROM vazifalar WHERE matn = 'O''zbek "tirnoq"'
pg-format hujum: SELECT * FROM "vazifalar""; DROP TABLE x; --"
client.escapeIdentifier: "a""b" | escapeLiteral: 'O''zbek'

Ikkinchi qatorga qarang: hujum matni vazifalar"; DROP TABLE x; -- butunlay bitta nom bo'lib qoldi — "vazifalar""; DROP TABLE x; --" degan g'alati jadval. Bunday jadval yo'q, Postgres xato beradi; DROP bajarilmaydi. pg ning o'zida ham shunday funksiyalar bor: client.escapeIdentifier() va client.escapeLiteral().

Qoida: qiymat — $1, nom — oq ro'yxat, oq ro'yxat imkonsiz bo'lsa — %I. %L (qiymatni matnga escape qilish) — faqat parametr ishlamaydigan joyda (masalan DDL buyruqlari).

5. Ro'yxatlar: = ANY($1)

"5, 12 va 999-vazifalarni ber". IN ($1, $2, $3) yozish uchun parametrlar sonini har safar yig'ish kerak. Postgres'da qulayrog'i bor — bitta parametr, massiv:

js
await client.query(
  "SELECT id, matn FROM vazifalar " +
    "WHERE id = ANY($1::bigint[]) ORDER BY id",
  [[2, 5, 999]],
);

Kanon chiqishi:

text
= ANY($1::bigint[]): 2 qator [{"id":"2","matn":"Postgres'ni Docker'da ko'tarish #o'qish #docker"},{"id":"5","matn":"Ingliz tili: 20 ta yangi so'z #o'qish #ingliz"}]
bo'sh massiv: 0 qator []

pg JavaScript massivini Postgres massiviga o'giradi, ::bigint[] — turini aniq aytamiz. 999 yo'q — 2 qator. Bo'sh massiv — 0 qator, xato emas. IN () esa bo'sh bo'lsa sintaksis xatosi berardi — ANY bu chekka holatni ham hal qiladi.

6. Dinamik WHERE

6.1 Muammo

API'da filtrlar ixtiyoriy: ?bajarildi=false, ?teg=#o'qish, ?q=bozor — har biri bor yoki yo'q. Sakkizta kombinatsiya uchun sakkizta so'rov yozmaymiz. Shartlarni yig'amiz — lekin qiymatlarni yopishtirmasdan.

Naqsh: ikki massiv — shartlar (SQL bo'laklari, $N bilan) va qiymatlar. Har yangi shart: qiymatni massivga qo'shamiz, uning raqami — $${qiymatlar.length}. Bu funksiya bazaga ulanmaydi — faqat matn va massiv yig'adi, shuning uchun uni shu yerda ishga tushirib ko'rish mumkin:

js
const likeEscape = (s) => s.replace(/[\\%_]/g, (m) => "\\" + m);

function sorovniQur(filtr) {
  const shartlar = ["egasi_id = $1"];
  const qiymatlar = [filtr.egasi];
  const qosh = (sql, qiymat) => {
    qiymatlar.push(qiymat);
    shartlar.push(sql.replace("?", `$${qiymatlar.length}`));
  };
  if (filtr.bajarildi !== undefined) {
    qosh("bajarildi = ?", filtr.bajarildi);
  }
  if (filtr.qidiruv) {
    qosh("matn ILIKE '%' || ? || '%'", likeEscape(filtr.qidiruv));
  }
  return {
    text: `SELECT id, matn FROM faol_vazifalar
WHERE ${shartlar.join(" AND ")} ORDER BY id`,
    values: qiymatlar,
  };
}

const s = sorovniQur({ egasi: 1, qidiruv: "' OR 1=1 --" });
console.log(s.text);
console.log(s.values);

Konsolda:

text
SELECT id, matn FROM faol_vazifalar
WHERE egasi_id = $1 AND matn ILIKE '%' || $2 || '%' ORDER BY id
[ 1, "' OR 1=1 --" ]

bajarildi berilmadi — shart qo'shilmadi, shuning uchun qidiruv $2 bo'ldi ($3 emas). Hujum matni ' OR 1=1 -- — values da, oddiy qiymat. SQL matnida foydalanuvchidan kelgan bitta ham harf yo'q. Natija { text, values } obyekt — pg aynan shu shaklni qabul qiladi: client.query(s).

6.2 Kanonning to'liq varianti

Kanon quruvchisi teg filtrini ham qo'shadi (subquery — Subquery va LATERAL). Kanon chiqishi:

text
dinamik: SELECT id, matn FROM faol_vazifalar WHERE egasi_id = $1 AND bajarildi = $2 AND id IN (SELECT vt.vazifa_id FROM vazifa_teglari vt JOIN teglar t ON t.id = vt.teg_id WHERE t.nom = $3) AND matn ILIKE '%' || $4 || '%' ORDER BY id [1,false,"#o'qish","' OR 1=1 --"]
dinamik natija: 0 qator []
dinamik (faqat teg): 3 qator [{"id":"1",…},{"id":"2",…},{"id":"34",…}]

To'rtta shart, to'rtta parametr, hujum so'zi — 0 qator. Faqat teg bilan — Azizning #o'qish tegli 3 ta vazifasi.

6.3 Bosqichli misol: o'zingiz to'ldiring

Muddat bo'yicha filtr qo'shamiz: ?muddatgacha=2026-11-01 — muddati shu sanadan oldingi vazifalar. sorovniQur ichiga yangi shart:

js
if (filtr.muddatgacha) {
  qosh("muddat < ?", filtr.[___:muddatgacha]);
}

Agar faqat egasi va muddatgacha berilsa, muddat sharti $ bo'ladi, values esa [1, "2026-11-01"].

7. Hujumchi nigohi

Hujumchi ilovangizda SQL'ga tushadigan har kirishni sinaydi:

  1. Qidiruv, filtr, saralash — URL parametrlari (?q=, ?saralash=). Avval bitta ' yuboradi: javob 500 va ichida syntax error bo'lsa — teshik topildi. Shuning uchun xato matnini hech qachon foydalanuvchiga ko'rsatmang (Global error handler).
  2. Ko'r injection (blind) — javobda ma'lumot ko'rinmasa ham: ' AND pg_sleep(5) -- — javob 5 soniya kechiksa, kod bajarilyapti. Keyin harfma-harf ma'lumot tortadi.
  3. Ikkinchi darajali (second-order) — zararli matn avval xavfsiz saqlanadi (parametr bilan), keyin boshqa joyda (masalan admin hisoboti) yopishtirilib ishlatiladi. Bazadan kelgan matn ham "foydalanuvchi kiritgani" — u ham faqat parametr bilan.
  4. Nom o'rnidagi kirish — ?saralash=id;DROP… — oq ro'yxat to'xtatadi.

Bizning himoya qatlamlari:

Qatlam Nimani to'xtatadi
$1 parametr (extended protokol) qiymat orqali har qanday kod; ; bilan ikkinchi buyruq
Oq ro'yxat / %I nom o'rnidagi kod
likeEscape joker bilan "hammasini topish"
Zod (25-qism) noto'g'ri tur va format kirishda
Minimal huquqli DB foydalanuvchisi teshik qolsa ham DROP qila olmaydi (Rollar va huquqlar, 27-qism)

Oxirgi qator haqida: hozir biz postgres superuser bilan ulanyapmiz — bu faqat lokal tajriba uchun. Production'da ilova faqat kerakli jadvallarga SELECT/INSERT/UPDATE/DELETE huquqi bor rol bilan ulanadi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
syntax error at or near "$1" jadval/ustun nomi parametrda oq ro'yxat yoki %I
ORDER BY $1 saralamaydi parametr — doimiy qiymat oq ro'yxat
cannot insert multiple commands into a prepared statement parametrli so'rovda ; bilan ikki buyruq alohida query chaqiruvlari
% hammasini topadi LIKE jokeri likeEscape
could not determine data type of parameter $1 Postgres turini taxmin qila olmadi $1::text, $1::bigint[]
${} bilan "faqat raqam" yopishtirildi "raqam" — foydalanuvchidan baribir $1

Oxirgi qator haqida: "bu id, u doim raqam" deb yopishtirish — eng keng tarqalgan bahona. id URL'dan keladi, unga 1 OR true yozish mumkin. Kanon postgres.js'ning sql.unsafe bilan aynan shuni ko'rsatdi: egasi 1 o'rniga hamma 48 qator (postgres.js va boshqa driverlar).

9. Mashqlar

Mashqlar kurs/mashqlar/26/42-injection/ da.

1-mashq (oson): xavflimi?

Har qatorga "xavfsiz" yoki "xavfli" deb yozing:

  • query("SELECT * FROM vazifalar WHERE id = $1", [id]) —
  • query(`SELECT * FROM vazifalar WHERE id = ${id}`) —
  • query(`SELECT * FROM vazifalar ORDER BY ${SARALASH[k]}`) (k tekshirilgan) —
  • query("SELECT * FROM " + jadval) (jadval — URL'dan) —
Yechim
  • xavfsiz — qiymat parametrda.
  • xavfli — ${} qiymatni SQL matniga yopishtiradi; id = "1 OR true" hamma qatorni beradi.
  • xavfsiz — yopishtirilgan matn biznikidan (oq ro'yxat), foydalanuvchi faqat kalit tanlaydi. k tekshirilmasa — undefined yopishadi va so'rov sintaksis xatosi beradi (hasOwn bilan tekshiring).
  • xavfli — nom foydalanuvchidan; oq ro'yxat yoki %I kerak.

2-mashq (o'rta): IN ro'yxatini ANY ga

Quyidagi kod parametrlarni qo'lda sanaydi va bo'sh ro'yxatda sintaksis xatosi beradi. Uni bitta parametrli = ANY ga qayta yozing — natija { text, values } obyekt bo'lsin. Bazasiz: obyektni chiqaring, ikki holatda — [2, 5] va [].

js
const joylar = idlar.map((_, i) => `$${i + 1}`).join(", ");
const text = `SELECT id, matn FROM vazifalar WHERE id IN (${joylar})`;
Yechim
js
function idlarBoyicha(idlar) {
  return {
    text: "SELECT id, matn FROM vazifalar " +
      "WHERE id = ANY($1::bigint[]) ORDER BY id",
    values: [idlar],
  };
}

console.log(idlarBoyicha([2, 5]).values); // [[2, 5]]
console.log(idlarBoyicha([]).values); // [[]]
console.log(idlarBoyicha([2, 5]).text.includes("$2")); // false

values — bitta element, u massiv: [[2, 5]]. Ichki qavslar shu sababli. SQL matni ro'yxat uzunligiga bog'liq emas — har doim $1. Bo'sh ro'yxatda kanon 0 qator oldi, xato emas.

3-mashq (qiyin): xavfsiz qidiruv funksiyasi

vazifaQidir(pool, egasi, soz, saralash) funksiyasini yozing: faol_vazifalar dan egasining vazifalari, soz bo'yicha ILIKE (escape bilan), saralash — "yangi" | "alifbo" (oq ro'yxat, Object.hasOwn). Azizni (1) uchta so'z bilan sinang: "bozor", "%", "' OR 1=1 --" — "alifbo" saralash bilan; oxirida "constructor" saralashini ham sinang. Har natijada qatorlar sonini va id larni chiqaring.

Yechim
js
import pg from "pg";

const SARALASH = {
  yangi: "yaratilgan_vaqt DESC, id DESC",
  alifbo: "lower(matn), id",
};
const likeEscape = (s) => s.replace(/[\\%_]/g, (m) => "\\" + m);

async function vazifaQidir(pool, egasi, soz, saralash) {
  if (!Object.hasOwn(SARALASH, saralash)) {
    throw new Error(`noma'lum saralash: ${saralash}`);
  }
  const { rows } = await pool.query(
    `SELECT id FROM faol_vazifalar
     WHERE egasi_id = $1 AND matn ILIKE '%' || $2 || '%'
     ORDER BY ${SARALASH[saralash]}`,
    [egasi, likeEscape(soz)],
  );
  return rows.map((q) => q.id);
}

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});
for (const soz of ["bozor", "%", "' OR 1=1 --"]) {
  const idlar = await vazifaQidir(pool, 1, soz, "alifbo");
  console.log(JSON.stringify(soz), idlar.length, idlar.join(","));
}
try {
  await vazifaQidir(pool, 1, "bozor", "constructor");
} catch (e) {
  console.log(e.message);
}
await pool.end();

Biz ishga tushirdik (holat 40, PostgreSQL 17.6, Node 24.21.0):

text
"bozor" 1 48
"%" 0 
"' OR 1=1 --" 0 
noma'lum saralash: constructor

"bozor" — bitta: 48-vazifa. Azizda xuddi shu matnli 8-vazifa ham bor, lekin u Savatda (yumshoq o'chirilgan), faol_vazifalar uni ko'rsatmaydi. "%" — escape ishladi, 0 qator. Hujum so'zi — 0 qator. constructor — hasOwn to'xtatdi; oddiy SARALASH[k] === undefined tekshiruvi bu yerda funksiya olib, SQL'ga function Object() { [native code] } ni yopishtirardi.

10. Node qadami: PgOmbori filtrlari

Avval tajriba: pg-tajriba papkasida xato qidir(soz) funksiyasini yozing va «Hujumni ko'ramiz» bo'limidagi uchta so'zni bering (DELETE hujumini faqat BEGIN … ROLLBACK ichida). Keyin xuddi shu so'zlarni $1 bilan bering — har biri 0 qator.

Diqqat: Injection'ni faqat o'z kompyuteringizdagi, o'zingizga tegishli bazada sinang. Boshqa birovning saytida "tekshirib ko'rish" — O'zbekiston Jinoyat kodeksi bo'yicha kompyuter axborotiga ruxsatsiz kirish hisoblanadi.

Endi loyiha. Oldingi darsda PgOmbori ning o'qish qismini yozdik. GET /v1/vazifalar ro'yxati 25-qismdagi filtrlarni qabul qiladi: ?bajarildi=, ?teg=, ?q= (qidiruv), sanalar va ?sort=. Ularning hammasi SQL'ga shu darsdagi uch qoida bilan tushadi.

10.1 Qiymatlar — Parametrlar klassi

«Dinamik WHERE» bo'limidagi qosh funksiyasining kanondagi shakli — kichik klass. U qiymatni massivga qo'shadi va SQL'ga faqat $n ni qaytaradi:

ts
class Parametrlar {
  readonly qiymatlar: unknown[] = [];
  qosh(x: unknown): string {
    this.qiymatlar.push(x);
    return `$${String(this.qiymatlar.length)}`;
  }
}

Filtr shartlari shu klass bilan yig'iladi (kanon shartlar funksiyasidan, qisqartirilgan). Nimaga qarang: SQL matnida faqat p.qosh(…) qaytargan $n lar bor.

ts
const s = [
  `v.egasi_id = ${p.qosh(egasi)}`,
  "v.ochirilgan_vaqt IS NULL",
];
if (filtr.bajarildi !== undefined) {
  s.push(`v.bajarildi = ${p.qosh(filtr.bajarildi)}`);
}
if (filtr.teglar !== undefined && filtr.teglar.length > 0) {
  const teglar = [...new Set(filtr.teglar)].map((t) => `#${t}`);
  // hamma teg bo'lsin (AND): mos teglar soni = so'ralganlar soni
  s.push(`(SELECT count(*) FROM vazifa_teglari vt
    JOIN teglar t ON t.id = vt.teg_id
    WHERE vt.vazifa_id = v.id
      AND t.nom = ANY(${p.qosh(teglar)}::text[]))
    = ${p.qosh(teglar.length)}`);
}
if (filtr.qidiruv !== undefined) {
  const soz = p.qosh(filtr.qidiruv);
  s.push(`strpos(lower(v.matn), lower(${soz})) > 0`);
}

Uch qaror:

  • Teg filtri — = ANY($n::text[]), «Ro'yxatlar» bo'limidagidek: nechta teg kelmasin, bitta parametr. "Hammasi bo'lsin" sharti — mos teglar sonini so'ralganlar soniga tenglashtirish.
  • Qidiruv — LIKE emas, strpos. strpos(matn, so'z) > 0 — "so'z matn ichida bormi". Unda joker belgilar umuman yo'q: % va _ oddiy harf. Shuning uchun likeEscape ham kerak emas. (Kichik ro'yxatda tezlik farqi sezilmaydi; katta jadvaldagi qidiruv — GIN, GiST, BRIN, pg_trgm darsida.)
  • egasi_id — har so'rovda birinchi shart. Boshqa foydalanuvchining vazifasi hech qanday filtr bilan ko'rinmaydi.

10.2 Nomlar — USTUN oq ro'yxati

Saralash maydoni (?sort=-yaratildi) — ustun nomi, parametr bo'la olmaydi. Kanon oq ro'yxati API maydonini SQL ifodasiga bog'laydi:

ts
type Ifoda = { sql: string; tur: string };
const USTUN: Record<SaralashMaydoni, Ifoda> = {
  id: { sql: "v.id", tur: "bigint" },
  tartib: { sql: "v.tartib", tur: "integer" },
  matn: { sql: 'v.matn COLLATE "C"', tur: "text" },
  bajarildi: { sql: "v.bajarildi", tur: "boolean" },
  yaratildi: { sql: "v.yaratilgan_vaqt", tur: "timestamptz" },
  yangilandi: { sql: "v.yangilangan_vaqt", tur: "timestamptz" },
};

function ustun(maydon: string): Ifoda {
  // "constructor", "__proto__" prototipdan kelmasin
  if (!Object.hasOwn(USTUN, maydon)) {
    throw new Error(`saralash maydoni oq ro'yxatda yo'q: ${maydon}`);
  }
  return USTUN[maydon as SaralashMaydoni];
}

Object.hasOwn — «Oq ro'yxat» bo'limidagi ogohlantirishning tuzatilgan shakli.

COLLATE "C" matnni harf kodlari bo'yicha saralaydi. Shunda tartib xotira ombori (JavaScript <) va sahifalash kursori bilan bir xil bo'ladi. Collation tartibni o'zgartirishini ORDER BY va LIMIT darsida ko'rgan edik. tur — kursor qiymatining SQL turi, u Keyset pagination darsida kerak bo'ladi.

10.3 Injection testi

test/pg-ombor.test.ts dagi test hujum so'zlarini to'g'ridan API ombori orqali yuboradi:

ts
await o.yarat({ matn: "100% tayyor #ish" });
await o.yarat({ matn: "Boshqa" });
const r = (filtr: object) => o.royxat({
  filtr, saralash: [], limit: 10, sahifa: { tur: "kursor" },
});
assert.equal(
  (await r({ qidiruv: "' OR 1=1 --" })).vazifalar.length, 0,
);
// % — oddiy belgi, joker emas
assert.deepEqual(
  (await r({ qidiruv: "%" })).vazifalar.map((v) => v.id), [1],
);
assert.equal(
  (await r({ teglar: ["ish' OR '1'='1"] })).vazifalar.length, 0,
);
await assert.rejects(o.royxat({
  filtr: {},
  saralash: [{ maydon: "constructor" as "id", kamayish: false }],
  limit: 10, sahifa: { tur: "kursor" },
}), /oq ro'yxatda yo'q/);

To'rt hujum, to'rt kutilgan natija: hujum so'zi — 0 qator; % — faqat haqiqatan % bor vazifa (1-vazifa); teg ichidagi qo'shtirnoq — 0 qator; constructor — xato. Biz holat 44 testlarini PostgreSQL 17.6 da ishga tushirdik — o'tdi:

text
✔ injection: qidiruv va teg — parametr, saralash maydoni —
  oq ro'yxat (Object.hasOwn) (117.704ms)

(Test nomi uzun — ikki qatorga bo'lindi.)

Kanon jonli serverda ham sinadi (?teg=o'qish&sort=-yaratildi — 200, teglar to'g'ri). Commit — PgBouncer darsida, holat 44 bilan birga.

11. Real ishda

  • OWASP Top 10 ro'yxatida injection ko'p yillardan beri bor: 2021 da A03, 2025 nashrida A05 (owasp.org, 2026-10-24 da ko'rildi). Ko'plab ma'lumot o'g'irlanishlari aynan shu xatodan boshlangan.
  • ORM va query builder'lar (Prisma, Drizzle, Kysely — 27-qism) ichida parametr ishlatadi, lekin ularning ham "xom SQL" eshigi bor ($queryRawUnsafe, sql.raw) — u yerda bu darsdagi hamma qoida qaytadi.
  • Kod review'da birinchi qidiriladigan narsa: SQL satri ichida ${ yoki +. Ko'p jamoalar buni linter qoidasi bilan tutadi.
  • Intervyuda: "SQL injection nima va undan qanday himoyalanasiz?", "Ustun nomini parametr qilsa bo'ladimi?", "Prepared statement injection'dan nega himoya qiladi?"

Xulosa

  • Injection sababi doim bitta: qiymat SQL matniga yopishtirilgan. Kanon: 48 qator, 8 email, o'chirilgan izohlar.
  • Qiymat — faqat $1, $2 bilan: extended protokolda qiymat koddan alohida keladi va bitta buyruqdan ortig'i qabul qilinmaydi.
  • Nom (jadval, ustun, ASC/DESC) parametr bo'lmaydi — oq ro'yxat (Object.hasOwn) yoki pg-format %I.
  • LIKE jokerlari parametrda ham ishlaydi — likeEscape.
  • Ro'yxat — = ANY($1::bigint[]); ixtiyoriy filtrlar — shartlar + qiymatlar massivlari.

Keyingi dars: postgres.js va boshqa driverlar — tagged template parametrni o'zi qo'yadigan drayver, sql.unsafe eshigi va parallel so'rovlarda 17 baravar tezlik farqi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
SQL injection va parametrli so'rovlar: $1, whitelist va dinamik WHERE — IlmHamroh