Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Hujumni ko'ramiz
- 2.1 Uch hujum — uch natija
- 2.2 Nega uchinchi hujum ishladi: ikki protokol
- 3. Himoya: parametr ($1)
- 3.1 To'g'ri qidiruv
- 3.2 Parametr qayerda ishlaydi, qayerda yo'q
- 3.3 LIKE dagi joker belgilar
- 4. Ustun va jadval nomlari: oq ro'yxat
- 4.1 Oq ro'yxat (whitelist)
- 4.2 pg-format va escape funksiyalari
- 5. Ro'yxatlar: = ANY($1)
- 6. Dinamik WHERE
- 6.1 Muammo
- 6.2 Kanonning to'liq varianti
- 6.3 Bosqichli misol: o'zingiz to'ldiring
- 7. Hujumchi nigohi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): xavflimi?
- 2-mashq (o'rta): IN ro'yxatini ANY ga
- 3-mashq (qiyin): xavfsiz qidiruv funksiyasi
- 10. Node qadami: PgOmbori filtrlari
- 10.1 Qiymatlar — Parametrlar klassi
- 10.2 Nomlar — USTUN oq ro'yxati
- 10.3 Injection testi
- 11. Real ishda
- Xulosa
- Manbalar
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;$1bilan — 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, $2bilan yuborasiz va nega bu himoya qilishini tushuntirasiz. LIKEdagi%va_ni escape qilasiz.- Ustun/jadval nomini oq ro'yxat yoki
pg-format %Ibilan 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:
// ❌ 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:
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:
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.pgbuni qiymatlar massivi bo'lmaganda ishlatadi. - Extended query (kengaytirilgan) — uch bosqich:
Parse(SQL shabloni,$1bilan),Bind(qiymatlar alohida),Execute. Faqat bitta buyruq.pgbuniquery(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: ExecuteDiagrammaga 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.
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
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:
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:
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:
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:
const likeEscape = (s) => s.replace(/[\\%_]/g, (m) => "\\" + m);
console.log(likeEscape("100%")); // 100\%
console.log(likeEscape("a_b")); // a\_b
console.log(likeEscape("bozor")); // bozorPostgres'da LIKE ning sukut escape belgisi — \. Avval \ ning o'zini ikkilaymiz (regex'dagi \\), keyin % va _.
Tekshirib ko'ring: Parametr bilan yozilgan
WHERE email = $1ga 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.
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:
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]dakalit = "constructor"yoki"toString"bo'lsa, oddiy obyekt prototipdan funksiya qaytaradi —undefinedemas! Kanon tajribasidagi kod buni tekshirmagan edi; API'ga ko'chirishda tuzatildi («Node qadami» bo'limi). Ishonchli variant:Object.hasOwn(SARALASH, kalit)yokinew Map(...), yoki obyektniObject.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:
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:
await client.query(
"SELECT id, matn FROM vazifalar " +
"WHERE id = ANY($1::bigint[]) ORDER BY id",
[[2, 5, 999]],
);Kanon chiqishi:
= 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:
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:
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:
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:
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:
- Qidiruv, filtr, saralash — URL parametrlari (
?q=,?saralash=). Avval bitta'yuboradi: javob 500 va ichidasyntax errorbo'lsa — teshik topildi. Shuning uchun xato matnini hech qachon foydalanuvchiga ko'rsatmang (Global error handler). - 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. - 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.
- 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]}`)(ktekshirilgan) —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.
ktekshirilmasa —undefinedyopishadi va so'rov sintaksis xatosi beradi (hasOwnbilan tekshiring). - xavfli — nom foydalanuvchidan; oq ro'yxat yoki
%Ikerak.
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 [].
const joylar = idlar.map((_, i) => `$${i + 1}`).join(", ");
const text = `SELECT id, matn FROM vazifalar WHERE id IN (${joylar})`;Yechim
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")); // falsevalues — 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
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):
"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:
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.
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 —
LIKEemas,strpos.strpos(matn, so'z) > 0— "so'z matn ichida bormi". Unda joker belgilar umuman yo'q:%va_oddiy harf. Shuning uchunlikeEscapeham 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:
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:
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:
✔ 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, $2bilan: 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) yokipg-format %I. LIKEjokerlari parametrda ham ishlaydi —likeEscape.- Ro'yxat —
= ANY($1::bigint[]); ixtiyoriy filtrlar —shartlar+qiymatlarmassivlari.
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
- PostgreSQL 18 hujjati — Frontend/Backend Protocol, Extended Query: https://www.postgresql.org/docs/18/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
- PostgreSQL 18 hujjati — Pattern Matching (
LIKE, escape belgisi): https://www.postgresql.org/docs/18/functions-matching.html - node-postgres — Queries, Parameterized query: https://node-postgres.com/features/queries
- pg-format: https://github.com/datalanche/node-pg-format
- OWASP — SQL Injection Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- Kanon:
tajriba/t42-injection.mjs,natijalar/tajriba/t42-injection.txt(PostgreSQL 18.6, Node 24.21.0, pg 8.23.1, 2026-10-24). 3-mashq — biz ishga tushirdik (PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!