Mundarija (40)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 Kirgan — hali hammasi mumkin degani emas
- 1.2 OWASP ro'yxatining birinchi qatori
- 1.3 Bizning kodimizda ham shunday teshiklar bor edi
- 2. RBAC: rol va ruxsat
- 2.1 Tushunchalar
- 2.2 Rol iyerarxiyasi
- 3. 401, 403 va 404
- 3.1 Uch xil "yo'q"
- 3.2 Begona resurs — nega 404, 403 emas
- 4. IDOR: hujum va himoya
- 4.1 Hujum bitta qatorda
- 4.2 Qayerlarda unutiladi
- 4.3 IDOR testlari
- 5. Bazaning o'zi ikkinchi qatlam: RLS ostida API
- 5.1 Muammo: API superuser edi
- 5.2 Yechim: ikki rol, ikki hovuz (pool)
- 5.3 Topilma: default privileges hammasini berib qo'ygan edi
- 5.4 Topilma: RLS ostida PUT 500 berardi
- 5.5 Narxi
- 6. Admin yo'llari
- 6.1 Rol har so'rovda bazadan
- 6.2 rolKerak va admin router
- 6.3 Markaziy tekshiruv: bloklangan hech qayerdan kirmaydi
- 7. Imzolangan kursor
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi kod?
- 2-mashq (o'rta): egalik bilan ombor
- 3-mashq (qiyin): IDOR test generatori
- 11. Loyiha qadami: rollar, admin va RLS ostida API
- 11.1 Migratsiyalar
- 11.2 Kod o'zgarishlari
- 11.3 Testlar
- 11.4 Commit
- 12. Real ishda
- Xulosa
- Manbalar
RBAC va resurs egaligi: rollar, 401 va 403, IDOR testlari va RLS
Qisqacha: Avtorizatsiya — kirgan odam nima qila olishini tekshirish. Ikki savol bor: "uning roli bunga ruxsat beradimi?" (RBAC) va "bu resurs unikimi?" (egalik). Ikkinchisini unutish — API'lardagi eng ko'p uchraydigan zaiflik: IDOR (BOLA), ya'ni URL'dagi raqamni o'zgartirib, birovning ma'lumotini olish. Himoya: har so'rovda egalik shartini qo'yish, rolni bazadan olish va har resurs uchun "begona foydalanuvchi — 404" testini yozish. Kanon bundan tashqari bazaning o'zini ham ikkinchi qatlam qildi: API endi superuser emas, RLS ostidagi oddiy rol bilan ulanadi.
Bu darsda
- Autentifikatsiya va avtorizatsiya farqini, 401, 403 va 404 qachon qaytishini tushuntira olasiz.
- Rol, ruxsat (permission) va rol iyerarxiyasini kodda ifodalaysiz.
- IDOR (BOLA) hujumini ko'rasiz va unga qarshi test yozasiz.
- API'ni RLS ostida, ikki xil cheklangan rol bilan ulaysiz.
vazifalar-apiga admin yo'llari, bloklash va imzolangan kursorni qo'shasiz.
Oldin bilishingiz kerak: Session-based auth, Status kodlarni to'g'ri tanlash, Row Level Security, Rollar va huquqlar, Ikki faktorli autentifikatsiya va passkeys.
Oldingi o'n beshta darsda bitta savolga javob berdik: "bu kim?". Parol, sessiya, SMS, Google, Telegram, 2FA — hammasi autentifikatsiya. Bugun ikkinchi savol: "bu odam shuni qila oladimi?".
1. Nega bu kerak?
1.1 Kirgan — hali hammasi mumkin degani emas
Parolni xavfsiz saqlash darsida ikki so'zni ajratgan edik: autentifikatsiya (kimligini tasdiqlash) va avtorizatsiya (ruxsat). Aeroportdagi o'xshatish: pasport nazorati — autentifikatsiya; bortga chiqish talonidagi o'rindiq raqami — avtorizatsiya. Pasportingiz haqiqiy bo'lsa ham, biznes klassga o'tira olmaysiz.
vazifalar-api da avtorizatsiya ikki xil bo'ladi:
- funksiya darajasida: "admin sahifasiga kim kira oladi?" — bu rolga bog'liq;
- obyekt darajasida: "42-vazifani kim o'qiy oladi?" — bu egalikka bog'liq.
1.2 OWASP ro'yxatining birinchi qatori
OWASP API Security Top 10 (2023) ro'yxatida birinchi o'rinda API1: Broken Object Level Authorization (BOLA) turibdi. Uning eski, keng tarqalgan nomi — IDOR (Insecure Direct Object Reference). Mexanizmi juda oddiy:
GET /v1/vazifalar/1 ← Aziz o'z vazifasini oladi
GET /v1/vazifalar/5 ← Aziz raqamni o'zgartirdiAgar server ikkinchi so'rovda ham 200 qaytarsa — bu IDOR. Hech qanday murakkab hujum kerak emas, faqat brauzer manzil qatori. Funksiya darajasidagi xato (oddiy foydalanuvchi admin yo'liga kira oladi) esa API5: Broken Function Level Authorization (BFLA) deyiladi.
1.3 Bizning kodimizda ham shunday teshiklar bor edi
28-qism kanonini qurishda haqiqiy zaifliklar topildi — hammasi oldingi qismlardagi bizning kodimizda:
- Idempotency-Key (Idempotency-Key darsi) hamma foydalanuvchi uchun umumiy edi. Dilnoza Azizning kalitini yuborsa, Azizning saqlangan javobini olardi.
- Eksport ishlari egasiz edi: havolani bilgan har kim boshqaning eksport faylini yuklab olardi.
- 27-qismdan beri API bazaga superuser (
postgres) bilan ulanardi. Row Level Security darsidagi siyosatlar bu rolga ishlamaydi.
Birinchi ikkitasi sessiya darsi bilan birga tuzatilgan va testlari bor (pastda ko'rasiz). Uchinchisi — bugungi darsning markazi.
2. RBAC: rol va ruxsat
2.1 Tushunchalar
RBAC (Role-Based Access Control) — rolga asoslangan ruxsat:
- ruxsat (permission) — bitta aniq amal:
vazifa:oqish,hisob:bloklash; - rol (role) — ruxsatlar to'plami:
foydalanuvchi,admin; - foydalanuvchiga ruxsat emas, rol beriladi.
Nega bunday bilvosita? Yangi xodim kelsa, unga 15 ta ruxsatni alohida berish o'rniga bitta rol berasiz. "Adminlar endi hisoblarni o'chira olsin" desangiz — bitta rolni o'zgartirasiz.
2.2 Rol iyerarxiyasi
Ko'pincha rollar bir-birini meros qiladi: admin oddiy foydalanuvchi qila oladigan hamma narsani qila oladi va yana bir oz ko'proq. Buni rol iyerarxiyasi deyishadi.
const ROLLAR = {
mehmon: { meros: [], ruxsatlar: ["vazifa:oqish-ommaviy"] },
foydalanuvchi: {
meros: ["mehmon"],
ruxsatlar: ["vazifa:oqish", "vazifa:yozish"],
},
admin: {
meros: ["foydalanuvchi"],
ruxsatlar: ["hisob:royxat", "hisob:bloklash"],
},
};
function ruxsatlari(rol, korilgan = new Set()) {
if (korilgan.has(rol)) return new Set();
korilgan.add(rol);
const r = ROLLAR[rol];
const hammasi = new Set(r.ruxsatlar);
for (const ota of r.meros) {
for (const x of ruxsatlari(ota, korilgan)) hammasi.add(x);
}
return hammasi;
}
const mumkinmi = (rol, ruxsat) => ruxsatlari(rol).has(ruxsat);
console.log([...ruxsatlari("admin")].join(", "));
console.log(mumkinmi("foydalanuvchi", "vazifa:yozish"));
console.log(mumkinmi("foydalanuvchi", "hisob:bloklash"));
console.log(mumkinmi("admin", "vazifa:oqish"));Konsolda:
hisob:royxat, hisob:bloklash, vazifa:oqish, vazifa:yozish, vazifa:oqish-ommaviy
true
false
trueAdmin o'z ruxsatlaridan tashqari foydalanuvchi va mehmon ruxsatlarini ham oldi. korilgan to'plami — ehtiyot chorasi: kimdir xato bilan admin ni foydalanuvchi ning merosiga ham yozsa, cheksiz rekursiya bo'lmaydi.
Kanon vazifalar-api bundan soddaroq: ikkita rol (foydalanuvchi, admin) va har yo'lda "qaysi rollarga ruxsat" ro'yxati. Kichik loyihada bu yetarli. Ruxsatlar jadvali va iyerarxiya rollar soni o'sganda kerak bo'ladi.
Diqqat: Frontend'dagi tekshiruv ("admin bo'lmasa tugmani yashir") — faqat qulaylik (UX) uchun. Tugmani yashirish yo'lni yopmaydi: kimdir
curlbilan to'g'ridan-to'g'ri so'rov yuboradi. Har ruxsat serverda tekshiriladi.
3. 401, 403 va 404
3.1 Uch xil "yo'q"
Status kodlarni to'g'ri tanlash darsidagi ikki kod endi to'liq ma'no oladi:
| Kod | Ma'nosi | Qachon |
|---|---|---|
| 401 Unauthorized | "kimligingizni bilmayman" | sessiya yo'q yoki yaroqsiz |
| 403 Forbidden | "bilaman, lekin ruxsat yo'q" | rol mos emas |
| 404 Not Found | "bunday narsa yo'q" | begona resurs ham |
Nomlar chalkash: 401 "Unauthorized" deyilgani bilan, aslida "unauthenticated" ma'nosini beradi. Bu HTTP tarixidagi qoldiq.
function holat(sessiya, kerakliRollar) {
if (sessiya === undefined) return 401;
if (!kerakliRollar.includes(sessiya.rol)) return 403;
return 200;
}
const SOROVLAR = [
["mehmon", undefined],
["Aziz", { id: 1, rol: "foydalanuvchi" }],
["Bekzod", { id: 7, rol: "admin" }],
];
for (const [kim, s] of SOROVLAR) {
console.log(kim, holat(s, ["admin"]));
}Konsolda:
mehmon 401
Aziz 403
Bekzod 200Tartib muhim: avval "kim?", keyin "ruxsat bormi?". Kirmagan odamga 403 qaytarsangiz, u "kirsam ham bo'lmaydi" deb o'ylaydi — aslida u shunchaki kirishi kerak.
3.2 Begona resurs — nega 404, 403 emas
Dilnoza Azizning 1-vazifasini so'radi. To'g'ri javob 403 ("ruxsat yo'q") emasmi?
403 bir narsani oshkor qiladi: "1-vazifa bor, lekin sizniki emas". Hujumchi 1 dan 1 000 000 gacha so'rab, qaysi id'lar bandligini, nechta vazifa borligini bilib oladi. 404 esa hech narsa aytmaydi: "sizning dunyongizda bunday vazifa yo'q". Kanon qoidasi: begona resurs — 404. 403 faqat funksiya darajasida (rol mos emas) qaytadi — u yerda yo'l mavjudligi sir emas.
Bitta so'rov uchala savoldan shu tartibda o'tadi:
flowchart LR
S[So'rov] --> A{Sessiya bormi?}
A -->|yo'q| K401[401]
A -->|ha| B{Rol mos keladimi?}
B -->|yo'q| K403[403]
B -->|ha| C{"Resurs topildimi? (egasi_id va RLS)"}
C -->|yo'q yoki begona| K404[404]
C -->|ha| K200[200]4. IDOR: hujum va himoya
4.1 Hujum bitta qatorda
IDOR odatda shunday kodda tug'iladi:
const VAZIFALAR = [
{ id: 1, egasiId: 1, matn: "Azizning vazifasi" },
{ id: 5, egasiId: 2, matn: "Dilnozaning vazifasi" },
];
// zaif: faqat id bo'yicha
function olZaif(id) {
return VAZIFALAR.find((v) => v.id === id);
}
// to'g'ri: id VA egasi bo'yicha
function ol(id, egasiId) {
return VAZIFALAR.find((v) => v.id === id && v.egasiId === egasiId);
}
const javob = (v) => (v === undefined ? "404" : `200 ${v.matn}`);
const aziz = 1;
console.log(javob(olZaif(5)));
console.log(javob(ol(5, aziz)));
console.log(javob(ol(1, aziz)));
console.log(javob(ol(999, aziz)));Konsolda:
200 Dilnozaning vazifasi
404
200 Azizning vazifasi
404Birinchi qator — IDOR: Aziz Dilnozaning vazifasini o'qidi. To'g'ri funksiyada begona id va mavjud bo'lmagan id bir xil javob beradi (404) — tashqaridan ularni farqlab bo'lmaydi.
SQL'da bu WHERE id = $1 o'rniga WHERE id = $1 AND egasi_id = $2. vazifalar-api da Sessiya darsidan beri PgOmbori har so'rovda egasi_id ni qo'shadi: ombor obyekti foydalanuvchi id bilan yaratiladi va boshqasini bilmaydi.
4.2 Qayerlarda unutiladi
Asosiy CRUD yo'llarida egalikni deyarli hamma eslaydi. Teshiklar chetda qoladi:
- yordamchi jadvallar: idempotentlik keshi, eksport ishlari, fayllar, bildirishnomalar — kanon topgan ikkala BOLA aynan shunday edi;
- ommaviy amallar:
PUT /vazifalar(hammasini almashtirish) ichidagi id'lar; - ichki havolalar:
GET /eksportlar/abc123/fayl— "havolani hech kim bilmaydi" degan taxmin; - yangi yo'l: kod nusxalanadi, egalik sharti esa yo'qoladi.
Shuning uchun egalikni ko'z bilan emas, test bilan tekshiramiz.
4.3 IDOR testlari
Kanon test/sessiya.test.ts da alohida to'plam bor — "foydalanuvchilar ajratilgan (IDOR / BOLA)". G'oyasi: ikki foydalanuvchi, biri resurs yaratadi, ikkinchisi uni har usul bilan olishga urinadi. Birinchi test (telefon uchun bo'lingan):
test("Aziz va Dilnoza: har biri faqat o'z vazifalari; begona id — " +
"404 (403 emas); PATCH/DELETE begonaga — 404", async () => {
const aziz = await royxat("aziz@example.com");
const dilnoza = await royxat("dilnoza@example.com");
const v = (await request(app).post("/v1/vazifalar")
.set("Cookie", aziz).send({ matn: "Azizniki #maxfiy" })
.expect(201)).body as { id: number };
await request(app).post("/v1/vazifalar").set("Cookie", dilnoza)
.send({ matn: "Dilnozaniki" }).expect(201);
const royxatD = (await request(app).get("/v1/vazifalar")
.set("Cookie", dilnoza).expect(200)).body as {
vazifalar: { matn: string }[];
};
assert.deepEqual(royxatD.vazifalar.map((x) => x.matn),
["Dilnozaniki"]);
const yol = `/v1/vazifalar/${String(v.id)}`;
await request(app).get(yol).set("Cookie", dilnoza).expect(404);
await request(app).patch(yol).set("Cookie", dilnoza)
.send({ bajarildi: true }).expect(404);
await request(app).delete(yol).set("Cookie", dilnoza).expect(404);
const teglar = (await request(app).get("/v1/teglar")
.set("Cookie", dilnoza).expect(200)).body as {
teglar: unknown[];
};
assert.deepEqual(teglar.teglar, []);
const a = (await request(app).get(yol).set("Cookie", aziz)
.expect(200)).body as { bajarildi: boolean };
assert.equal(a.bajarildi, false);
});E'tibor bering, test faqat "o'qiy olmaydi" bilan to'xtamaydi:
- ro'yxatda begona vazifa yo'q;
GET,PATCH,DELETE— uchalasi 404;- teglar ro'yxatida Azizning
#maxfiytegi ko'rinmaydi (yon kanal orqali sizish); - oxirida Aziz o'z vazifasini o'zgarmagan holda ko'radi — Dilnozaning
PATCHurinishi jim muvaffaqiyat bo'lmagan.
Biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z PostgreSQL 17.6 bazamiz), faqat shu to'plamni:
node --test --test-name-pattern="IDOR" test/sessiya.test.tsChiqishda to'rtta ✔ qatoriga qarang — har biri bitta resurs turida begona foydalanuvchini to'xtatadi:
▶ foydalanuvchilar ajratilgan (IDOR / BOLA)
✔ Aziz va Dilnoza: har biri faqat o'z vazifalari; begona id — 404 (403 emas); PATCH/DELETE begonaga — 404 (692.5733ms)
✔ Idempotency-Key — foydalanuvchi bo'yicha: Dilnoza Azizning kalitini ishlatsa Azizning javobini OLMAYDI (498.517ms)
✔ eksport ishi — faqat yaratgan foydalanuvchiga (begona — 404) (601.3384ms)
✔ yashirin yo'llar yo'q: /eksportlar, /auth versiyasiz — 404 (faqat /vazifalar, /teglar eski yo'l sifatida qoldi) (448.51ms)
✔ foydalanuvchilar ajratilgan (IDOR / BOLA) (2243.1847ms)
ℹ tests 4
ℹ suites 1
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4549.3041Nima bo'ldi? Vazifalar, Idempotency-Key va eksport ishlarida begona foydalanuvchi 404 oldi — yuqoridagi uchta teshikdan ikkitasi test bilan yopiq. To'rtinchi test — yana bir topilma: /v1 router versiyasiz ildizda ham ochiq edi, ya'ni /eksportlar /v1/eksportlar bilan bir xil ishlardi. Bunday "unutilgan" yo'llar OWASP API ro'yxatida alohida band — API9 (inventarni noto'g'ri boshqarish).
Maslahat: Har yangi resurs (izohlar, fayllar, kalitlar) uchun shu shablonni nusxa qiling: "ikki foydalanuvchi, begona — 404, ro'yxatda yo'q". Bu test bir marta yoziladi va har refaktoringda sizni himoya qiladi.
5. Bazaning o'zi ikkinchi qatlam: RLS ostida API
5.1 Muammo: API superuser edi
Egalik sharti kodda. Lekin bitta yangi so'rovda WHERE egasi_id unutilsa-chi? Row Level Security darsida bazaga siyosat qo'ygan edik: vazifalar_ilova roli faqat egasi_id = joriy_foydalanuvchi() qatorlarni ko'radi. Lekin API qaysi rol bilan ulanardi? Quyidagi so'rovlar kanon sxemasi (119–120 migratsiyalari bilan) to'ldirilgan lokal bazada bajarildi (biz ishga tushirdik, PostgreSQL 17.6):
SELECT usename, rolsuper, rolbypassrls
FROM pg_user JOIN pg_roles ON rolname = usename
WHERE usename = current_user;Natija:
usename | rolsuper | rolbypassrls
----------+----------+--------------
postgres | t | t
(1 row)Bu — bizning psql sessiyamiz, lekin 27-qism DATABASE_URL ham aynan postgres foydalanuvchisi edi (kanon jonli API'da tekshirdi). rolbypassrls = t — RLS siyosatlari bu rolga umuman ta'sir qilmaydi:
SELECT count(*) AS jami, count(DISTINCT egasi_id) AS egalar
FROM vazifalar;Natija:
jami | egalar
------+--------
48 | 7
(1 row)WHERE siz so'rov — yetti kishining 48 ta vazifasi. RLS bor edi, lekin qulf eshikka emas, devorga osilgan edi.
5.2 Yechim: ikki rol, ikki hovuz (pool)
Kanon migratsiyasi 119-rollar-rls-api.sql API'ni ikki cheklangan rolga ajratdi — eng kam huquq tamoyili (Xavfsizlik tamoyillari):
| Rol | Nima qila oladi | Nima qila olmaydi |
|---|---|---|
vazifalar_api |
vazifalar, RLS ostida | auth jadvallari, CREATE |
vazifalar_auth |
foydalanuvchilar, sessiyalar, sirlar | vazifalar |
Nega ikkita? Login paytida server foydalanuvchini email bo'yicha topishi kerak — u hali kimligini bilmaydi, RLS ostida esa hech narsa ko'rinmaydi. Shuning uchun auth uchun alohida rol, uning foydalanuvchilar jadvalida o'z siyosati bor (hamma qator). Lekin bu rol vazifalarni umuman ko'rmaydi. Ma'lumot roli esa sessiya tokenlari va 2FA sirlarini ko'rmaydi.
Endi vazifalar_api bilan:
SET ROLE vazifalar_api;
SELECT count(*) FROM vazifalar;Natija:
SET
count
-------
0
(1 row)set_config siz — nol qator. Bu "yopiq holatda xavfsiz" (fail closed): kod foydalanuvchini o'rnatishni unutsa, hech narsa ko'rinmaydi, birovniki ham.
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
SELECT count(*) AS jami, count(DISTINCT egasi_id) AS egalar
FROM vazifalar;
ROLLBACK;Natija:
BEGIN
SET
set_config
------------
1
(1 row)
jami | egalar
------+--------
13 | 1
(1 row)
ROLLBACKXuddi o'sha WHERE siz so'rov endi faqat Azizning 13 ta vazifasini qaytaradi. set_config(..., true) — uchinchi argument true = faqat shu tranzaksiya uchun (SET LOCAL ning funksiya ko'rinishi; SET LOCAL o'zi parametr qabul qilmaydi).
Rollar chegarasi:
SET ROLE vazifalar_api;
SELECT count(*) FROM sessiyalar;Natija:
SET
ERROR: permission denied for table sessiyalarSET ROLE vazifalar_auth;
SELECT count(*) FROM vazifalar;Natija:
SET
ERROR: permission denied for table vazifalar5.3 Topilma: default privileges hammasini berib qo'ygan edi
Yuqoridagi permission denied — 119 dan keyin. Undan oldin (sessiya darsidan shu darsgacha) vazifalar_api sessiya tokenlari hash'larini, shifrlangan TOTP sirlarini va OTP kodlarini o'qiy olardi. Sabab — 27-qismdagi 106-rollar.sql:
ALTER DEFAULT PRIVILEGES FOR ROLE CURRENT_USER IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO vazifalar_ilova;Bu buyruq "keyin yaratiladigan har jadvalga ham shu huquqlar" degani. 28-qismdagi har yangi auth jadvali avtomatik ravishda ma'lumot roliga ochilgan. O'zingiz ko'ring — hozir ham yangi jadval yaratsak:
CREATE TABLE sinov_sirlar (x text);
SELECT string_agg(privilege_type, ', ' ORDER BY privilege_type)
FROM information_schema.role_table_grants
WHERE grantee = 'vazifalar_ilova' AND table_name = 'sinov_sirlar';
DROP TABLE sinov_sirlar;Natija:
CREATE TABLE
string_agg
--------------------------------
DELETE, INSERT, SELECT, UPDATE
(1 row)
DROP TABLEHech kim GRANT yozmadi, lekin to'rtta huquq tayyor. Qulaylik uchun qo'yilgan sozlama xavfsizlik teshigiga aylandi. 119 ning yechimi — auth jadvallaridan REVOKE ALL ... FROM vazifalar_ilova, keyingi har auth migratsiyasida ham (masalan, keyingi darsdagi api_kalitlar) shu qator.
Bu teshik xato bermasdi — shuning uchun uzoq yashadi. Kanon uni information_schema.role_table_grants ni ko'rib chiqib topdi. Saboq: huquqlarni "ishlayapti" bilan emas, "ortiqcha nima bor" bilan tekshiring.
5.4 Topilma: RLS ostida PUT 500 berardi
RLS yoqilgach yana bir yashirin xato chiqdi. PUT /v1/vazifalar (hammasini almashtirish) mijoz bergan id'larni ishlatadi. Agar id boshqa foydalanuvchiniki bo'lsa — yangi id beriladi. Buni aniqlash uchun eski kod "begona id'larni" SELECT qilardi. RLS ostida esa begona qator ko'rinmaydi — so'rov bo'sh qaytadi, kod "bo'sh id" deb o'ylaydi va INSERT qiladi:
"begona id'lar" so'rovi: 0 qator (RLS B ning qatorini yashirdi — kod uni "bo'sh id" deb o'ylaydi)
INSERT → duplicate key value violates unique constraint "vazifalar_pkey" ⇒ API 500 bo'lardiBu — kanon tajribasining chiqishi. Yechim — faqat "band-mi?" savoliga javob beradigan funksiya: vazifa_idlari_band(bigint[]). U SECURITY DEFINER (egasining huquqi bilan ishlaydi, RLS'siz), lekin faqat id'larni qaytaradi, mazmunni emas:
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
SELECT id FROM vazifalar WHERE id IN (1, 5, 999);
SELECT vazifa_idlari_band('{1, 5, 999}');
ROLLBACK;Natija:
BEGIN
SET
set_config
------------
1
(1 row)
id
----
1
(1 row)
vazifa_idlari_band
--------------------
{1,5}
(1 row)
ROLLBACKOddiy SELECT faqat 1 ni ko'rdi (5 — Dilnozaniki). Funksiya esa "1 va 5 band, 999 bo'sh" dedi. SECURITY DEFINER funksiyada SET search_path = pg_catalog, public qotirilgan — aks holda chaqiruvchi o'z sxemasidagi soxta vazifalar jadvalini qo'yib, funksiyani aldashi mumkin edi. EXECUTE huquqi faqat vazifalar_ilova ga.
Va nihoyat, RLS'ning WITH CHECK qismi — begona nomiga yozish:
\set VERBOSITY terse
BEGIN;
SET LOCAL ROLE vazifalar_api;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
INSERT INTO vazifalar (egasi_id, matn) VALUES (2, 'begona nomiga');
ROLLBACK;Natija:
BEGIN
SET
set_config
------------
1
(1 row)
ERROR: new row violates row-level security policy for table "vazifalar"
ROLLBACK"Yangi qator vazifalar jadvalidagi RLS siyosatini buzadi". Kod egasi_id ni noto'g'ri qo'ysa ham, baza rad etadi.
5.5 Narxi
Endi har o'qish ham tranzaksiyada: BEGIN + set_config + so'rov + COMMIT. Kanon o'lchadi (400 marta ketma-ket, mediana):
- pool.query, tranzaksiyasizeski, superuser1,01 ms
- BEGIN+set_config+so'rov+COMMITsuperuser3,24 ms
- xuddi shu, vazifalar_apiRLS ostida4,2 ms
Manba: kanon o'lchovi (IlmHamroh, 2026-10): PostgreSQL 18.6 Docker Desktop orqali, Node 24.21.0, i5-12500H, Windows 11, 2026-10-28
Bitta vazifa 1.0 ms dan 3.2 ms ga oshdi — lekin bu superuser bilan ham. Ya'ni narx RLS siyosatida emas, uchta qo'shimcha tarmoq aylanishida (har biri Docker Desktop'da ~0.7 ms). 50 ta vazifali ro'yxatda farq yo'qoldi. Superuser bilan 4.87 ms, RLS ostida 4.25 ms. set_config ni asosiy so'rov ichiga (CTE) qo'shib aylanishlarni kamaytirish mumkin edi — kanon buni qilmadi: Postgres siyosat va CTE baholash tartibini kafolatlamaydi.
Tekshirib ko'ring: Kodda
WHERE egasi_id = $1baribir qolgan. RLS bor ekan, uni olib tashlasa bo'ladimi?
Javob
Yo'q. Bu — chuqur himoya (defense in depth): ikki mustaqil qatlam. Ertaga kimdir test uchun superuser bilan ulansa yoki migratsiya siyosatni tasodifan o'chirsa, WHERE qoladi. Kod sharti qolsa, RLS qo'shimcha narx bermaydi — kanon o'lchovida farq tarmoq aylanishlaridan edi.
6. Admin yo'llari
6.1 Rol har so'rovda bazadan
Rol qayerda saqlanadi? foydalanuvchilar.rol ustunida (foydalanuvchi | admin, CHECK bilan). Sessiya tekshiruvi har so'rovda sessiyalar va foydalanuvchilarni JOIN qiladi va rolni ham oladi. Natija: adminlikdan olingan odam o'sha sessiya bilan keyingi so'rovdayoq 403 oladi.
Rolni JWT ichiga yozganda bunday emas: token muddati tugaguncha eski rol amal qiladi. Bu Session vs JWT qarori darsidagi kanon tanlovining yana bir sababi.
6.2 rolKerak va admin router
Kanon src/auth/admin.ts (telefon uchun bo'lingan, izohlar qisqartirilgan; bo'lingan nusxa tip va lint'dan o'tdi — biz tekshirdik):
// admin.ts — RBAC: administrator yo'llari. Rol sessiyadan (bazadan,
// har so'rovda). 401 — kim ekani noma'lum; 403 — kimligi ma'lum,
// RUXSATI yo'q. Admin boshqalarning VAZIFALARINI ko'rmaydi
// (maxfiylik) — faqat hisoblarni boshqaradi. Har amal jurnalga.
import { Router } from "express";
import type { RequestHandler } from "express";
import { z } from "zod";
import type { Pool } from "pg";
import { tekshirib } from "../umumiy/tekshir.ts";
import { IlovaXatosi, TopilmadiXatosi } from "../umumiy/xatolar.ts";
import { TURLAR } from "../vazifalar/pg-ombor.ts";
import { KirilmaganXatosi } from "./middleware.ts";
import type { AuthOmbori } from "./ombor.ts";
import type { SessiyaOmbori } from "./sessiya.ts";
export type Rol = "foydalanuvchi" | "admin";
export class RuxsatYoqXatosi extends IlovaXatosi {
constructor() {
super(403, "Bu amal uchun ruxsatingiz yo'q", {
kod: "ruxsat-yoq",
sarlavha: "Ruxsat yo'q",
});
}
}
/** Rol talabi: sessiya yo'q → 401, rol mos emas → 403 */
export function rolKerak(...rollar: readonly Rol[]): RequestHandler {
return (req, _res, next) => {
if (req.sessiya === undefined) throw new KirilmaganXatosi();
if (!rollar.includes(req.sessiya.rol)) {
throw new RuxsatYoqXatosi();
}
next();
};
}
const Id = z.strictObject({
id: z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER),
});
const Royxat = z.strictObject({
limit: z.coerce.number().int().min(1).max(100).default(20),
keyin: z.coerce.number().int().nonnegative().default(0),
});
export function adminRouteri(
pool: Pool,
ombor: AuthOmbori,
sessiyalar: SessiyaOmbori,
): Router {
const r = Router();
r.use(rolKerak("admin"), (_req, res, next) => {
res.set("Cache-Control", "no-store");
next();
});
// Hisoblar (keyset: id > keyin) — parol hash, telefon, sirlar
// QAYTARILMAYDI (ortiqcha maydon oshkor qilmaslik)
r.get("/foydalanuvchilar", tekshirib({ query: Royxat },
async ({ query }, _req, res) => {
const { rows } = await pool.query<Record<string, unknown>>({
text: `SELECT id, email, toliq_ism AS "toliqIsm", rol,
yaratilgan_vaqt AS yaratilgan,
email_tasdiqlangan_vaqt IS NOT NULL
AS "emailTasdiqlangan",
bloklangan_vaqt IS NOT NULL AS bloklangan
FROM foydalanuvchilar WHERE id > $1
ORDER BY id LIMIT $2`,
values: [query.keyin, query.limit + 1],
types: TURLAR,
});
const keyingi = rows.length > query.limit
? (rows[query.limit - 1]?.["id"] as number)
: null;
res.json({
foydalanuvchilar: rows.slice(0, query.limit),
keyingi,
});
}));
// Bloklash: kirish taqiqlanadi va BARCHA sessiyalari darhol bekor
r.post("/foydalanuvchilar/:id/bloklash", tekshirib({ params: Id },
async ({ params }, req, res) => {
const admin = req.sessiya;
if (admin === undefined) throw new KirilmaganXatosi();
if (params.id === admin.foydalanuvchi.id) {
throw new IlovaXatosi(409, "O'zingizni bloklay olmaysiz", {
kod: "ozini-bloklash",
sarlavha: "Mumkin emas",
});
}
const { rowCount } = await pool.query(
`UPDATE foydalanuvchilar
SET bloklangan_vaqt = coalesce(bloklangan_vaqt, now())
WHERE id = $1 AND rol <> 'admin'`, [params.id]);
if (rowCount !== 1) {
throw new TopilmadiXatosi(
"Foydalanuvchi topilmadi (yoki administrator)",
"foydalanuvchi-topilmadi", "Topilmadi");
}
const bekor = await sessiyalar.hammasiniBekorQil(params.id);
await ombor.hodisa({
tur: "admin_bloklash",
foydalanuvchiId: params.id,
ip: req.ip,
userAgent: req.get("user-agent"),
tafsilot: {
admin: admin.foydalanuvchi.id,
bekorSessiyalar: bekor,
},
});
res.sendStatus(204);
}));
r.post("/foydalanuvchilar/:id/blokdan-chiqarish",
tekshirib({ params: Id }, async ({ params }, req, res) => {
const { rowCount } = await pool.query(
"UPDATE foydalanuvchilar SET bloklangan_vaqt = NULL " +
"WHERE id = $1", [params.id]);
if (rowCount !== 1) {
throw new TopilmadiXatosi("Foydalanuvchi topilmadi",
"foydalanuvchi-topilmadi", "Topilmadi");
}
await ombor.hodisa({
tur: "admin_blokdan_chiqarish",
foydalanuvchiId: params.id,
ip: req.ip,
userAgent: req.get("user-agent"),
tafsilot: { admin: req.sessiya?.foydalanuvchi.id },
});
res.sendStatus(204);
}));
return r;
}To'rt qaror:
r.use(rolKerak("admin"))— router darajasida. Yangi admin yo'li qo'shilsa, tekshiruv avtomatik. Har yo'lga alohida yozish — unutish xavfi (BFLA).- Ortiqcha maydon yo'q. Admin ro'yxatida parol hash'i, telefon, sirlar yo'q. Hatto admin ham ko'rishi shart bo'lmagan narsani ko'rmaydi. Admin boshqalarning vazifalarini ham ko'rmaydi — buning uchun yo'l yo'q.
- O'zini va boshqa adminni bloklab bo'lmaydi. O'zini — 409. Boshqa adminni —
rol <> 'admin'sharti, natija 404. Adminlar urushi bo'lmaydi. - Bloklash darhol ishlaydi: sessiyalar bekor, sessiya tekshiruvi esa
bloklangan_vaqt IS NULLni ham talab qiladi.
6.3 Markaziy tekshiruv: bloklangan hech qayerdan kirmaydi
Kirishning yo'li ko'p: parol, SMS, Google, Telegram, passkey, 2FA. Har biriga "bloklanganmi?" shartini qo'yish — biri albatta unutiladi. Kanon uni bitta joyga qo'ydi — sessiya beruvchi sessiyaBer funksiyasiga:
async function sessiyaBer(
req: Request, res: Response, f: Foydalanuvchi, eslabQol: boolean,
): Promise<void> {
// bloklangan hisob — HECH QAYSI yo'l (parol, SMS, Google,
// Telegram, passkey, 2FA) sessiya olmaydi
if (await d.ombor.bloklanganmi(f.id)) {
await xizmat.hodisa({
tur: "bloklangan_kirish", foydalanuvchiId: f.id, ...mijoz(req),
});
throw new IlovaXatosi(403,
"Hisob bloklangan — administrator bilan bog'laning",
{ kod: "hisob-bloklangan", sarlavha: "Hisob bloklangan" });
}
// ... eski sessiyani bekor qilish, yangisini yaratish, cookie
}Bu yerda 403 to'g'ri: foydalanuvchi parolni to'g'ri kiritdi (kim ekani ma'lum), lekin kirishga ruxsat yo'q.
7. Imzolangan kursor
25-qismda sahifalash kursorini ochiq base64url(JSON) qilgan edik va "imzolash 28-qismda" degan edik (Pagination). Mijoz kursor ichini o'zgartirib, "o'zi yasagan" keyset holatini yubora olardi. Qiymatlar baribir SQL parametri — injection emas. Lekin kelajakda kursorga egasi kabi maydon qo'shilsa, bu IDOR'ga yo'l ochadi. Endi kursor HMAC bilan imzolanadi:
const enc = new TextEncoder();
const b64url = (b) => btoa(String.fromCharCode(...new Uint8Array(b)))
.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
const dan64url = (t) =>
atob(t.replaceAll("-", "+").replaceAll("_", "/"));
const kalit = await crypto.subtle.importKey(
"raw", enc.encode("faqat-namuna-kalit-32-bayt-uzun!"),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
async function imzo(t) {
const mac = await crypto.subtle.sign("HMAC", kalit, enc.encode(t));
return b64url(mac.slice(0, 16));
}
async function kursorYoz(qiymatlar, sorov) {
const json = JSON.stringify({ v: qiymatlar, i: sorov });
const t = b64url(enc.encode(json));
return `${t}.${await imzo(t)}`;
}
async function kursorOqi(kursor, sorov) {
const [t, berilgan] = kursor.split(".");
if (!t || berilgan !== (await imzo(t))) return undefined;
const ichki = JSON.parse(dan64url(t));
return ichki.i === sorov ? ichki.v : undefined;
}
const k = await kursorYoz([4, 9], "teg=oqish");
console.log(k);
console.log(await kursorOqi(k, "teg=oqish"));
console.log(await kursorOqi(k, "teg=oila"));
const [, imzoQismi] = k.split(".");
const soxtaIchki = b64url(enc.encode(
JSON.stringify({ v: [0, 0], i: "teg=oqish" })));
const soxta = `${soxtaIchki}.${imzoQismi}`;
console.log(await kursorOqi(soxta, "teg=oqish"));Konsolda:
eyJ2IjpbNCw5XSwiaSI6InRlZz1vcWlzaCJ9.u7S7RByFAHdP777Qc_BDlw
[4, 9]
undefined
undefinedIkkinchi qator — to'g'ri kursor. Uchinchisi — kursor boshqa filtr (teg=oila) bilan ishlatildi: rad. To'rtinchisi — ichi o'zgartirildi, imzo eskisi: rad. HMAC'ning 16 bayti (128 bit) yetarli va kursorni qisqa tutadi. Brauzer misolida qisqalik uchun !== bilan solishtirdik; kanon kodida — timingSafeEqual. Kalit ILOVA_SIRI dan HKDF bilan olinadi (maqsadi kursor); misoldagi kalit — faqat namuna.
8. Hujumchi nigohi
Har qatorda: hujum, himoya va uni qotiradigan test.
| Hujum | Himoya | Test |
|---|---|---|
| URL'dagi id ni o'zgartirish (IDOR) | AND egasi_id, RLS |
IDOR to'plami: begona — 404 |
| begona Idempotency-Key | kalit + foydalanuvchi | Dilnoza Azizning javobini olmaydi |
| admin yo'liga oddiy sessiya bilan | rolKerak router darajasida |
401 va 403 testi |
| 403/404 farqi bilan id'larni sanash | begonaga ham 404 | "404 (403 emas)" |
WHERE unutilgan yangi so'rov |
vazifalar_api + RLS |
rls.test.ts izolyatsiya |
| ma'lumot roli bilan sessiya hash'lari | REVOKE, ikki rol |
rollar chegarasi |
| bloklangan hisob boshqa yo'ldan | sessiyaBer da markaziy |
bloklash: darhol 401, kirish 403 |
| kursor ichini o'zgartirish | HMAC kursor | o'zgartirilgan kursor — 400 |
9. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
WHERE id = $1 (egasiz) |
IDOR | AND egasi_id = $2 |
| begona resursga 403 | id'lar mavjudligi oshkor | 404 |
| kirmaganga 403 | mijoz noto'g'ri xulosa qiladi | 401 |
| rol JWT ichida, muddati uzoq | adminlikdan olingan ham admin | rol bazadan, har so'rovda |
| rol tekshiruvi har yo'lda alohida | bittasi unutiladi | router darajasida |
| API superuser bilan | RLS ishlamaydi | cheklangan rol |
ALTER DEFAULT PRIVILEGES ni unutish |
yangi jadvallar ochiq | REVOKE, huquqlarni ko'rib chiqish |
| admin javobida hamma ustun | parol hash'i sizadi | aniq ustunlar ro'yxati |
10. Mashqlar
Mashqlar kurs/mashqlar/28/20-rbac/ da.
1-mashq (oson): qaysi kod?
Har holat uchun 401, 403 yoki 404 ni tanlang:
- Cookie'siz
GET /v1/admin/foydalanuvchilar. - Aziz (oddiy foydalanuvchi)
GET /v1/admin/foydalanuvchilar. - Dilnoza
DELETE /v1/vazifalar/1(Azizniki). - Aziz
GET /v1/vazifalar/999(bunday id yo'q). - Bloklangan Sardor to'g'ri parol bilan
POST /v1/auth/kirish.
Yechim
- 401 — kim ekani noma'lum.
- 403 — kim ekani ma'lum, rol mos emas.
- 404 — begona resurs; 403 mavjudlikni oshkor qilardi.
- 404 — 3-holat bilan bir xil javob, tashqaridan farqlanmaydi.
- 403
hisob-bloklangan— parol to'g'ri (kimligi ma'lum), lekin kirish taqiqlangan.
2-mashq (o'rta): egalik bilan ombor
Xotiradagi kichik ombor yozing: omborYarat(egasiId) funksiyasi ol(id), yangila(id, matn) va ochir(id) usullari bor obyekt qaytarsin. Har usul faqat shu egasining vazifalari bilan ishlasin, begonasida undefined (yoki false) qaytarsin. Umumiy massiv: { id: 1, egasiId: 1, matn: "Azizniki" } va { id: 5, egasiId: 2, matn: "Dilnozaniki" }. Dilnozaning omborida 1-vazifani o'qish, yangilash va o'chirishga urining, keyin Azizning omborida 1-vazifa o'zgarmaganini chiqaring.
Yechim
const VAZIFALAR = [
{ id: 1, egasiId: 1, matn: "Azizniki" },
{ id: 5, egasiId: 2, matn: "Dilnozaniki" },
];
function omborYarat(egasiId) {
const top = (id) =>
VAZIFALAR.find((v) => v.id === id && v.egasiId === egasiId);
return {
ol: (id) => top(id),
yangila(id, matn) {
const v = top(id);
if (v === undefined) return undefined;
v.matn = matn;
return v;
},
ochir(id) {
const i = VAZIFALAR.indexOf(top(id));
if (i === -1) return false;
VAZIFALAR.splice(i, 1);
return true;
},
};
}
const dilnoza = omborYarat(2);
console.log(dilnoza.ol(1));
console.log(dilnoza.yangila(1, "Buzildi"));
console.log(dilnoza.ochir(1));
console.log(dilnoza.ol(5).matn);
const aziz = omborYarat(1);
console.log(aziz.ol(1).matn, VAZIFALAR.length);Konsolda:
undefined
undefined
false
Dilnozaniki
Azizniki 2Asosiy g'oya — egasi ombor yaratilganda bir marta beriladi va hamma usul faqat top orqali ishlaydi. Yangi usul qo'shganda egalikni unutib bo'lmaydi: boshqa qidiruv yo'q. Kanon PgOmbori ham shunday: new PgOmbori(pool, egasiId). indexOf(undefined) → -1 — begona id'da o'chirish jim muvaffaqiyat bo'lmaydi.
3-mashq (qiyin): IDOR test generatori
Kichik "API" funksiyasini yozing: api(metod, yol, foydalanuvchiId) → status kod. Yo'llar: GET, PATCH, DELETE — /vazifalar/:id. Egalik tekshiruvi bo'lsin, lekin DELETE da ataylab unutilgan bo'lsin (xato). Keyin idorTest(resursId, egasi, begona) funksiyasini yozing: uchala metodni begona foydalanuvchi bilan sinab, 404 bo'lmaganlarini TESHIK deb chiqarsin. Ishora: yo'ldan id ni Number(yol.split("/")[2]) bilan oling.
Yechim
const VAZIFALAR = new Map([
[1, { egasiId: 1, matn: "Azizniki" }],
[5, { egasiId: 2, matn: "Dilnozaniki" }],
]);
function api(metod, yol, foydalanuvchiId) {
const id = Number(yol.split("/")[2]);
const v = VAZIFALAR.get(id);
if (v === undefined) return 404;
if (metod === "GET" || metod === "PATCH") {
return v.egasiId === foydalanuvchiId ? 200 : 404;
}
if (metod === "DELETE") {
VAZIFALAR.delete(id); // xato: egalik tekshirilmadi
return 204;
}
return 405;
}
function idorTest(resursId, egasi, begona) {
const yol = `/vazifalar/${resursId}`;
for (const metod of ["GET", "PATCH", "DELETE"]) {
const kod = api(metod, yol, begona);
console.log(metod, kod, kod === 404 ? "ok" : "TESHIK");
}
console.log("egasi ko'radimi:", api("GET", yol, egasi));
}
idorTest(1, 1, 2);Konsolda:
GET 404 ok
PATCH 404 ok
DELETE 204 TESHIK
egasi ko'radimi: 404Test ikki narsani ushladi: DELETE begonaga 204 qaytardi, va oxirgi qator — egasi endi o'z vazifasini ko'rmaydi, chunki begona uni o'chirib yubordi. Shuning uchun kanon testi oxirida egasi resursini o'zgarmagan holda ko'rishini ham tekshiradi. Testni GET bilan tugatsangiz, bunday "jim" zararni sezmay qolasiz.
11. Loyiha qadami: rollar, admin va RLS ostida API
11.1 Migratsiyalar
sql/119-rollar-rls-api.sql (= prisma/migrations/20261028110000_rollar_rls_api; schema.prisma ga rol, bloklanganVaqt). Beshta qism:
foydalanuvchilar.rol(CHECK (rol IN ('foydalanuvchi', 'admin')), sukutfoydalanuvchi) vabloklangan_vaqt.vazifalar_authroli (LOGIN,CONNECTION LIMIT 20): auth jadvallariga DML,foydalanuvchilaruchun o'z siyosatiUSING (true).REVOKE ALLsakkizta auth jadvalidanvazifalar_ilovadan.vazifa_idlari_band(bigint[])—SECURITY DEFINER,search_pathqotirilgan,EXECUTEfaqatvazifalar_ilova;vazifalar_id_seqgaSELECT, UPDATE._prisma_migrationsbo'lsa — ungaSELECT(migratsiya holati tekshiruvi uchun).
Funksiya qismi (kanon matni, izohsiz):
CREATE FUNCTION vazifa_idlari_band(idlar bigint[]) RETURNS bigint[]
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = pg_catalog, public
RETURN (SELECT coalesce(array_agg(id), '{}')
FROM vazifalar WHERE id = ANY(idlar));
REVOKE EXECUTE ON FUNCTION vazifa_idlari_band(bigint[]) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION vazifa_idlari_band(bigint[])
TO vazifalar_ilova;Parollar faylda yo'q: lokal bazada trust, serverda rollar parol bilan (scram-sha-256) va parol .env da. Namuna fayl sql/120-namuna-admin.sql — bitta qator: Bekzodni admin qiladi (faqat lokal; prodakshnda admin qo'lda tayinlanadi, migratsiyada emas).
11.2 Kod o'zgarishlari
src/auth/admin.ts— yuqoridagi modul;ilova.tsdav1.use("/admin", adminRouteri(...)).src/auth/sessiya.ts— sessiya tekshiruvif.rolni oladi vaf.bloklangan_vaqt IS NULLni talab qiladi;JoriySessiyaturigarol.src/auth/marshrut.ts—sessiyaBerda markaziy bloklash tekshiruvi.src/vazifalar/pg-ombor.ts— hamma o'qishlar ham tranzaksiyada. Umumiy#tranzaksiyadayordamchisiBEGINdan keyin bitta qator qo'shdi:
await c.query("BEGIN");
// RLS va audit (vazifa_tarixi.kim_id) — shu tranzaksiya uchun
// joriy foydalanuvchi. SET LOCAL parametr olmaydi — set_config
await c.query(
"SELECT set_config('ilova.foydalanuvchi_id', $1, true)",
[String(this.#egasi)],
); almashtirHammasi begona id'larni endi vazifa_idlari_band bilan aniqlaydi. PrismaOmbori da xuddi shu — interaktiv $transaction + $executeRaw bilan set_config (Prisma'da "har so'rov sozlamasi" yo'q).
src/server.ts— ikki hovuz (pool):DATABASE_URL(vazifalar_api) vaAUTH_DATABASE_URL(vazifalar_auth,application_name vazifalar-api-auth). Ikkinchisi berilmasa — bitta hovuz (lokal dev).src/umumiy/kursor.ts— HMAC kursor (kursorYoz,kursorOqi); kalitILOVA_SIRI→ HKDFkursor.
Bonus: 27-qismdan qolgan vazifa yopildi — vazifa_tarixi.kim_id endi yoziladi, chunki trigger set_config qiymatini o'qiydi.
Ulanish satrlari (lokal trust namunasi, parolsiz):
DATABASE_URL=postgres://vazifalar_api@127.0.0.1:5432/vazifalar
AUTH_DATABASE_URL=postgres://vazifalar_auth@127.0.0.1:5432/vazifalarKanon .env.example da AUTH_DATABASE_URL qism oxirida paydo bo'ladi, DATABASE_URL namunasi esa hali ham postgres foydalanuvchisi bilan qolgan. O'z loyihangizda ma'lumot hovuziga vazifalar_api ni yozing — bugungi darsning butun ma'nosi shunda. Kanonning jonli tekshiruvi aynan shu ikki rol bilan ishlagan.
11.3 Testlar
test/admin.test.ts (+5) va test/rls.test.ts (+15). RLS testlari 25–27-qismlardagi ombor shartnoma testlarini endi vazifalar_api roli bilan, RLS ostida, ikkala ombor (pg va Prisma) uchun qayta yurgizadi. Biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z PostgreSQL 17.6 bazamiz):
node --test test/admin.test.tsHar qatorda status kodlarga qarang: 401, 403 va 404 qayerda chiqqani:
✔ HMAC kursor: o'z kaliti bilan — ok; ichi o'zgartirilsa, boshqa kalit, imzosiz (25-qism formati), boshqa so'rov imzosi — undefined (4.7351ms)
✔ 401 (sessiyasiz) va 403 (oddiy foydalanuvchi); admin ro'yxati — parol hash, telefon YO'Q; keyset sahifalash (1081.2746ms)
✔ bloklash: sessiyasi DARHOL 401, parol bilan kirish 403 hisob-bloklangan; o'zini va boshqa adminni — yo'q; ochilgach — kiradi (1531.0287ms)
✔ rol har so'rovda bazadan: adminlikdan olinsa — o'sha sessiya bilan darhol 403 (1267.6166ms)
✔ API'dagi kursor imzolangan: o'zgartirilgan kursor — 400 (2474.9637ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9186.0024Nima bo'ldi? Beshala test o'tdi. Kirmaganga 401, oddiy foydalanuvchiga 403. Bloklangan hisob darhol chiqariladi. Rol olib tashlansa, o'sha sessiya ham 403 oladi. Soxta kursor — 400. rls.test.ts ham o'z bazamizda 15/15 o'tdi. Unda: ikki ombor shartnomasi (har biri 5 test), izolyatsiya, PUT begona id bilan, kim_id, rollar chegarasi va ikki rol bilan to'liq HTTP oqim (begona id — 404).
Kanon tekshiruvi (2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 204 (203 + 1 o'tkazilgan), Testcontainers 10, e2e 6/6, 12 migratsiya sql/ bilan bir xil. Jonli API endi haqiqiy rollar bilan ishlaydi: pg_stat_activity da vazifalar-api | vazifalar_api va vazifalar-api-auth | vazifalar_auth.
11.4 Commit
Tip, lint va testlar o'tgach — commit. Keyingi dars (ABAC) faqat tajriba, loyihaga hech narsa qo'shmaydi, shuning uchun holat shu yerda yopiladi:
M="feat(auth): RBAC (rol, admin yo'llari, bloklash), API ikki rol"
M+=" bilan — vazifalar_api (RLS, set_config) va vazifalar_auth;"
M+=" HMAC kursor; audit kim_id; db: 119 (+120 namuna)"
git add src test sql prisma
git commit -m "$M"12. Real ishda
- Multi-tenant tizimlarda (bitta ilova, ko'p kompaniya) egalik ikki qavatli: foydalanuvchi va tashkilot. Har so'rovda ikkalasi ham tekshiriladi.
- Freymvorklar rol tekshiruvini dekorator yoki "guard" bilan beradi (NestJS — 30-qismda). Lekin obyekt darajasidagi egalikni hech qaysi freymvork siz uchun yozmaydi.
- Supabase kabi platformalar frontend'dan bazaga to'g'ridan-to'g'ri so'rov yuboradi — u yerda RLS yagona himoya, shuning uchun siyosatlar juda puxta yoziladi.
- Intervyuda: "401 va 403 farqi?", "IDOR nima va uni qanday topasiz?", "Begona resursga nega 404?", "RLS'ni ilova bilan qanday ulaysiz?".
Xulosa
- Avtorizatsiya ikki daraja: funksiya (rol — RBAC) va obyekt (egalik).
- 401 — kim noma'lum; 403 — rol mos emas; begona resurs — 404 (mavjudlik oshkor bo'lmasin).
- IDOR — eng ko'p uchraydigan API zaifligi; himoyasi
AND egasi_idva har resurs uchun "begona — 404" testi. - Rol bazadan, har so'rovda; tekshiruv router darajasida; bloklash —
sessiyaBerda markaziy. - API superuser bo'lsa RLS ishlamaydi: ikki cheklangan rol, har tranzaksiyada
set_config, default privileges'niREVOKE. - Kursor HMAC bilan imzolanadi — mijoz uning ichini o'zgartira olmaydi.
Keyingi dars: ABAC, ReBAC va policy engine'lar — "faqat ish vaqtida", "ulashilgan vazifani faqat o'qish" kabi murakkab qoidalarni rol bilan ifodalab bo'lmaganda.
Manbalar
- OWASP API Security Top 10 (2023): API1 BOLA, API5 BFLA, API9 Improper Inventory Management — https://owasp.org/API-Security/editions/2023/en/0x11-t10/
- OWASP Authorization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- RFC 9110, 401 va 403 status kodlari: https://www.rfc-editor.org/rfc/rfc9110#name-401-unauthorized
- PostgreSQL 18: Row Security Policies — https://www.postgresql.org/docs/18/ddl-rowsecurity.html;
ALTER DEFAULT PRIVILEGES— https://www.postgresql.org/docs/18/sql-alterdefaultprivileges.html;SECURITY DEFINERxavfsizligi — https://www.postgresql.org/docs/18/sql-createfunction.html - Kanon o'lchovi (IlmHamroh, 2026-10-28): RLS narxi (PostgreSQL 18.6 Docker Desktop orqali, Node 24.21.0, i5-12500H), hujum yuzasi tajribasi, holat 20 tekshiruvi. Darsdagi brauzer misollari, SQL so'rovlar va
admin.test.ts,rls.test.ts, IDOR to'plami — biz ishga tushirdik (2026-10-28, PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!