9-bo‘lim
CSRF, SSRF va sessiya zaifliklari
Saytlararo so'rov soxtalashtirish, server tomonidagi so'rov soxtalashtirish, sessiya hujumlari va API xavfsizligi.
Ushbu bo‘lim mundarijasi
Bu bo'limda OWASP Top 10 ning qolgan muhim zaifliklarini ko'rib chiqamiz.
CSRF #
Saytlararo so'rov soxtalashtirish - foydalanuvchi nomidan uning xohishisiz amal bajarish.
<!-- Zararli saytdagi kod -->
<form action="https://bank.uz/otkazma" method="POST" id="soxta">
<input type="hidden" name="qabul_qiluvchi" value="hujumchi123">
<input type="hidden" name="summa" value="5000000">
</form>
<script>document.getElementById('soxta').submit();</script>
Himoya 1: CSRF tokeni #
final class CsrfHimoyasi
{
public function token(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public function tekshiring(?string $token): void
{
if ($token === null || !isset($_SESSION['csrf_token'])) {
throw new CsrfXatosi();
}
if (!hash_equals($_SESSION['csrf_token'], $token)) {
throw new CsrfXatosi();
}
}
public function maydon(): string
{
return sprintf(
'<input type="hidden" name="_token" value="%s">',
htmlspecialchars($this->token(), ENT_QUOTES, 'UTF-8'),
);
}
}
// Oraliq qatlam
final class CsrfOraliqQatlami
{
private const XAVFSIZ_METODLAR = ['GET', 'HEAD', 'OPTIONS'];
public function ishlang(Sorov $sorov, callable $keyingi): Javob
{
if (in_array($sorov->metod(), self::XAVFSIZ_METODLAR, true)) {
return $keyingi($sorov);
}
$token = $sorov->post('_token')
?? $sorov->sarlavha('X-CSRF-Token');
$this->csrf->tekshiring($token);
return $keyingi($sorov);
}
}
<form method="POST" action="/buyurtma/bekor">
<?= $csrf->maydon() ?>
<button type="submit">Bekor qilish</button>
</form>
// AJAX so'rovlarda
fetch('/api/buyurtma/42/bekor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
},
});
Himoya 2: SameSite cookie #
session_set_cookie_params([
'samesite' => 'Lax', // yoki 'Strict'
'secure' => true,
'httponly' => true,
]);
| Qiymat | Xatti-harakat |
|---|---|
Strict | Boshqa saytdan kelgan hech qanday so'rovda yuborilmaydi |
Lax | Faqat yuqori darajali GET navigatsiyada yuboriladi |
None | Har doim yuboriladi (Secure majburiy) |
Lax - amaliy tanlovStrict xavfsizroq, lekin noqulay: boshqa saytdagi havola orqali
kirgan foydalanuvchi tizimdan chiqqan ko'rinadi.
Lax esa POST, PUT, DELETE so'rovlarida cookie yubormaydi -
bu CSRF ning katta qismini to'sadi.
Zamonaviy brauzerlarda Lax standart qiymat.
- Eski brauzerlar uni qo'llab-quvvatlamaydi
- Bir domendagi subdomen orqali chetlab o'tish mumkin
GETorqali holat o'zgartirsangiz -Laxyordam bermaydi
Chuqur himoya: token va SameSite birga ishlatilsin.
Himoya 3: Origin tekshiruvi #
final class OriginTekshiruvchi
{
private const RUXSAT_ORIGINLAR = [
'https://dokon.uz',
'https://www.dokon.uz',
];
public function tekshiring(Sorov $sorov): void
{
$origin = $sorov->sarlavha('Origin')
?? $this->refererDanOrigin($sorov->sarlavha('Referer'));
if ($origin === null) {
throw new OriginYoq();
}
if (!in_array($origin, self::RUXSAT_ORIGINLAR, true)) {
throw new NotogriOrigin($origin);
}
}
private function refererDanOrigin(?string $referer): ?string
{
if ($referer === null) {
return null;
}
$qismlar = parse_url($referer);
if (!isset($qismlar['scheme'], $qismlar['host'])) {
return null;
}
return "{$qismlar['scheme']}://{$qismlar['host']}";
}
}
SSRF #
Server tomonidagi so'rov soxtalashtirish - serverni o'zi xohlagan manzilga so'rov yuborishga majburlash.
// ZAIF
$rasm = file_get_contents($_POST['rasm_url']);
rasm_url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
rasm_url=http://localhost:6379/
rasm_url=file:///etc/passwd
rasm_url=gopher://10.0.2.10:3306/_...
final class XavfsizUrlYuklovchi
{
private const RUXSAT_SXEMALAR = ['http', 'https'];
private const MAKS_HAJM = 10 * 1024 * 1024;
private const VAQT_CHEGARASI = 5;
public function yuklang(string $url): string
{
$this->urlTekshiring($url);
$kontekst = stream_context_create([
'http' => [
'timeout' => self::VAQT_CHEGARASI,
'follow_location' => 0, // yo'naltirishni o'zimiz boshqaramiz
'max_redirects' => 0,
'user_agent' => 'Dokon/1.0',
],
]);
$malumot = @file_get_contents($url, false, $kontekst, 0, self::MAKS_HAJM);
if ($malumot === false) {
throw new YuklashXatosi($url);
}
return $malumot;
}
private function urlTekshiring(string $url): void
{
$qismlar = parse_url($url);
if ($qismlar === false || !isset($qismlar['scheme'], $qismlar['host'])) {
throw new NotogriUrl($url);
}
if (!in_array($qismlar['scheme'], self::RUXSAT_SXEMALAR, true)) {
throw new NotogriUrl('Faqat HTTP va HTTPS');
}
// Nostandart portlarni taqiqlash
$port = $qismlar['port'] ?? ($qismlar['scheme'] === 'https' ? 443 : 80);
if (!in_array($port, [80, 443], true)) {
throw new NotogriUrl('Faqat 80 va 443 portlar');
}
// DNS ni yechib, IP ni tekshirish
$iplar = gethostbynamel($qismlar['host']);
if ($iplar === false) {
throw new NotogriUrl('Host topilmadi');
}
foreach ($iplar as $ip) {
if ($this->ichkiManzilmi($ip)) {
throw new NotogriUrl('Ichki manzillarga ruxsat yo\'q');
}
}
}
private function ichkiManzilmi(string $ip): bool
{
return !filter_var($ip, FILTER_VALIDATE_IP, [
'flags' => FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
]);
}
}
Hujumchi domenni shunday sozlaydi:
- Birinchi so'rovda tashqi IP qaytariladi (tekshiruv o'tadi)
- Ikkinchi so'rovda ichki IP qaytariladi (so'rov yuboriladi)
Himoya:
- IP ni bir marta yeching va shu IP ga ulaning
- Yoki tarmoq darajasida ichki manzillarni bloklang
# Ilova serveridan ichki tarmoqqa chiqishni taqiqlash
sudo ufw deny out to 169.254.0.0/16
sudo ufw deny out to 10.0.0.0/8
sudo ufw deny out to 172.16.0.0/12
sudo ufw deny out to 192.168.0.0/16
Barcha tashqi so'rovlarni alohida proksi xizmat orqali yuboring.
Bu xizmat:
- Alohida tarmoqda turadi
- Ichki tarmoqqa kira olmaydi
- Oq ro'yxat bilan ishlaydi
- Barcha so'rovlarni jurnalga yozadi
Sessiya hujumlari #
// To'liq xavfsiz sessiya sozlamalari
ini_set('session.use_strict_mode', '1'); // noma'lum ID qabul qilinmaydi
ini_set('session.use_only_cookies', '1'); // URL da ID bo'lmasin
ini_set('session.cookie_httponly', '1'); // JavaScript o'qiy olmaydi
ini_set('session.cookie_secure', '1'); // faqat HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.sid_length', '48');
ini_set('session.sid_bits_per_character', '6');
ini_set('session.gc_maxlifetime', '1800');
ini_set('session.name', 'DOKON_SESSIYA'); // standart nom emas
session_start();
final class SessiyaXavfsizligi
{
private const FAOLSIZLIK = 1800; // 30 daqiqa
private const MUTLAQ = 43200; // 12 soat
private const YANGILASH = 300; // har 5 daqiqada ID yangilanadi
public function kirishdanKeyin(Foydalanuvchi $f): void
{
// Fiksatsiyaga qarshi
session_regenerate_id(true);
$_SESSION = [
'foydalanuvchi_id' => $f->id,
'boshlangan' => time(),
'oxirgi_faollik' => time(),
'oxirgi_yangilash' => time(),
'barmoq_izi' => $this->barmoqIzi(),
];
}
public function tekshiring(): void
{
if (!isset($_SESSION['foydalanuvchi_id'])) {
throw new SessiyaYoq();
}
if (time() - $_SESSION['oxirgi_faollik'] > self::FAOLSIZLIK) {
$this->tugating();
throw new SessiyaMuddatiTugadi();
}
if (time() - $_SESSION['boshlangan'] > self::MUTLAQ) {
$this->tugating();
throw new SessiyaMuddatiTugadi();
}
if (!hash_equals($_SESSION['barmoq_izi'], $this->barmoqIzi())) {
$this->tugating();
$this->jurnal->warning('Sessiya barmoq izi mos kelmadi');
throw new ShubhaliSessiya();
}
// Muntazam ID yangilash - o'g'irlangan ID tez eskiradi
if (time() - $_SESSION['oxirgi_yangilash'] > self::YANGILASH) {
session_regenerate_id(true);
$_SESSION['oxirgi_yangilash'] = time();
}
$_SESSION['oxirgi_faollik'] = time();
}
private function barmoqIzi(): string
{
return hash('sha256',
($_SERVER['HTTP_USER_AGENT'] ?? '') .
($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '') .
$this->sir
);
}
public function tugating(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $p['path'],
'domain' => $p['domain'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
}
session_destroy();
}
}
Fayl sessiyalarining kamchiliklari:
- Bir necha serverda ishlamaydi
- Foydalanuvchining barcha sessiyalarini ko'rib bo'lmaydi
- Majburiy chiqarish qiyin
Bazada saqlansa:
CREATE TABLE sessiyalar (
id CHAR(64) PRIMARY KEY,
foydalanuvchi_id BIGINT UNSIGNED NULL,
ip VARCHAR(45),
agent VARCHAR(255),
malumot TEXT,
oxirgi_faollik TIMESTAMP,
KEY idx_foydalanuvchi (foydalanuvchi_id, oxirgi_faollik)
);
Endi foydalanuvchi "barcha qurilmalardan chiqish" tugmasini bosa oladi.
API xavfsizligi #
// API uchun CSRF kerak emas - agar cookie ishlatilmasa
final class ApiAutentifikatsiyasi
{
public function tekshiring(Sorov $sorov): Foydalanuvchi
{
$sarlavha = $sorov->sarlavha('Authorization')
?? throw new AvtorizatsiyaYoq();
if (!str_starts_with($sarlavha, 'Bearer ')) {
throw new NotogriFormat();
}
$token = substr($sarlavha, 7);
return $this->tokenlar->tekshiring($token)
?? throw new NotogriToken();
}
}
| Cookie | Bearer token | |
|---|---|---|
| Avtomatik yuboriladi | Ha | Yo'q |
| CSRF xavfi | Bor | Yo'q |
| XSS da o'g'irlash | HttpOnly bilan qiyin | Oson (localStorage) |
| Mobil ilova | Noqulay | Qulay |
| Bir domendagi SPA | Qulay | O'rtacha |
Veb-ilova uchun: HttpOnly cookie + CSRF token. Mobil va tashqi API uchun: Bearer token.
localStorage da saqlamanglocalStorage.setItem('token', token); // XSS da o'g'irlanadi
XSS zaifligi bo'lsa, hujumchi tokenni bir qatorda o'g'irlaydi.
Yaxshiroq variantlar:
- HttpOnly cookie - JavaScript umuman o'qiy olmaydi
- Xotirada saqlash (sahifa yangilanganda yo'qoladi)
- Qisqa muddatli token + yangilash tokeni cookie da
Massaviy tayinlash #
// ZAIF
$foydalanuvchi->toldiring($_POST);
$this->repozitoriy->saqlang($foydalanuvchi);
POST /profil
ism=Husanboy&[email protected]&rol=administrator&balans=999999
// XAVFSIZ - aniq ro'yxat
final class ProfilYangilashSorovi
{
private function __construct(
public readonly string $ism,
public readonly string $familiya,
public readonly ?string $telefon,
) {}
public static function sorovdan(array $malumot): self
{
return new self(
ism: self::matn($malumot['ism'] ?? '', 80),
familiya: self::matn($malumot['familiya'] ?? '', 80),
telefon: isset($malumot['telefon'])
? self::telefon($malumot['telefon'])
: null,
);
}
private static function matn(string $qiymat, int $maks): string
{
$tozalangan = trim($qiymat);
if ($tozalangan === '' || mb_strlen($tozalangan) > $maks) {
throw new ValidatsiyaXatosi();
}
return $tozalangan;
}
}
So'rov obyekti (DTO) faqat ruxsat etilgan maydonlarni oladi.
rol, balans, id kabi maydonlar unda umuman yo'q - shuning
uchun ularni o'zgartirib bo'lmaydi.
Biznes mantiq zaifliklari #
// ZAIF: narx mijozdan olinadi
$jami = $_POST['jami'];
$buyurtma = Buyurtma::yangi($mijozId, $jami);
// XAVFSIZ: narx serverda hisoblanadi
$jami = Pul::nol();
foreach ($sorov->elementlar as $element) {
$mahsulot = $this->mahsulotlar->toping($element->mahsulotId)
?? throw new MahsulotTopilmadi();
$jami = $jami->qoshing($mahsulot->narx->kopaytiring($element->soni));
}
Skanerlar SQL inyeksiyani topadi, lekin bu zaifliklarni topa olmaydi:
| Zaiflik | Misol |
|---|---|
| Manfiy miqdor | soni=-5 → balans oshadi |
| Narxni o'zgartirish | Yashirin maydondagi narx |
| Chegirma kuponini takrorlash | Bir kuponni 100 marta |
| Qadamni o'tkazib yuborish | To'lovsiz buyurtma tasdiqlash |
| Poyga sharti | Bir vaqtda ikki so'rov |
| Chegarani chetlab o'tish | Balansdan ko'p yechish |
Ularni faqat inson tahlili va yaxshi testlar topadi.
// Poyga sharti - qulflash bilan
$this->pdo->beginTransaction();
try {
$stmt = $this->pdo->prepare(
'SELECT balans FROM hisoblar WHERE id = ? FOR UPDATE'
);
$stmt->execute([$hisobId]);
$balans = (int) $stmt->fetchColumn();
if ($balans < $summa) {
throw new MablagYetarliEmas();
}
$stmt = $this->pdo->prepare(
'UPDATE hisoblar SET balans = balans - ? WHERE id = ?'
);
$stmt->execute([$summa, $hisobId]);
$this->pdo->commit();
} catch (Throwable $x) {
$this->pdo->rollBack();
throw $x;
}
FOR UPDATE bo'lmasa:
So'rov A: balansni o'qidi (100 000)
So'rov B: balansni o'qidi (100 000)
So'rov A: 100 000 yechdi → balans 0
So'rov B: 100 000 yechdi → balans -100 000
FOR UPDATE qatorni qulflaydi va ikkinchi so'rov kutadi.
Xavfsizlik sarlavhalari #
# Barcha muhim sarlavhalar
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" always;
# Server ma'lumotini yashirish
server_tokens off;
| Sarlavha | Nima qiladi |
|---|---|
X-Content-Type-Options | MIME turini taxmin qilishni to'sadi |
X-Frame-Options | Clickjacking dan himoya |
Referrer-Policy | URL ni tashqi saytga yubormaslik |
Strict-Transport-Security | Faqat HTTPS |
Permissions-Policy | Brauzer API lariga kirishni cheklash |
Content-Security-Policy | Resurs manbalarini cheklash |
Hujumchi sizning saytingizni ko'rinmas iframe ichiga joylashtiradi
va foydalanuvchini "Yutuq oling" tugmasini bosishga majbur qiladi -
aslida u "Hisobni o'chirish" tugmasini bosadi.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
# Sarlavhalarni tekshirish
curl -sI https://dokon.uz | grep -iE '^(x-|strict|content-security|referrer|permissions)'
CORS #
final class CorsOraliqQatlami
{
private const RUXSAT_ORIGINLAR = [
'https://dokon.uz',
'https://app.dokon.uz',
];
public function ishlang(Sorov $sorov, callable $keyingi): Javob
{
$origin = $sorov->sarlavha('Origin');
if ($origin === null || !in_array($origin, self::RUXSAT_ORIGINLAR, true)) {
return $keyingi($sorov);
}
$javob = $sorov->metod() === 'OPTIONS'
? new Javob(204)
: $keyingi($sorov);
return $javob
->sarlavhaBilan('Access-Control-Allow-Origin', $origin)
->sarlavhaBilan('Access-Control-Allow-Credentials', 'true')
->sarlavhaBilan('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->sarlavhaBilan('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token')
->sarlavhaBilan('Access-Control-Max-Age', '3600')
->sarlavhaBilan('Vary', 'Origin');
}
}
// XAVFLI: hamma origindan ruxsat + cookie
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');
Bu kombinatsiya brauzerlar tomonidan rad etiladi, lekin ba'zi dasturchilar buni "tuzatish" uchun Origin ni aks ettiradi:
// YANADA XAVFLI
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Credentials: true');
Endi istalgan sayt sizning API ingizga foydalanuvchi cookie lari bilan so'rov yubora oladi.
Har doim oq ro'yxat ishlating.
CORS brauzer cheklovi. U serverni himoya qilmaydi.
curl, Postman yoki boshqa server CORS ni umuman e'tiborsiz
qoldiradi.
CORS - bu "boshqa saytdagi JavaScript sizning API ingizga murojaat qila oladimi?" degan savolga javob, autentifikatsiya emas.
Himoya tekshiruv ro'yxati #
| Chora | Bajarildi |
|---|---|
| CSRF tokeni barcha o'zgartiruvchi so'rovlarda | |
SameSite=Lax yoki Strict | |
| Origin/Referer tekshiruvi | |
| Tashqi URL lar oq ro'yxat va IP tekshiruvi bilan | |
| Ichki tarmoqqa chiqish tarmoq darajasida bloklangan | |
session_regenerate_id(true) kirishdan keyin | |
HttpOnly, Secure, SameSite cookie da | |
| Sessiya muddati va faolsizlik cheklovi | |
| Massaviy tayinlash - DTO orqali | |
| Narx va miqdor serverda tekshiriladi | |
Poyga shartlari - FOR UPDATE bilan | |
| Xavfsizlik sarlavhalari qo'shilgan | |
| CORS oq ro'yxat bilan |
- CSRF himoyasini amalga oshiring (token + oraliq qatlam).
- CSRF hujumini o'z lokal muhitingizda sinab ko'ring.
SameSitecookie ni sozlang va farqni kuzating.XavfsizUrlYuklovchini yozing va ichki IP larni bloklang.- Sessiya xavfsizligi klassini yozing.
- Sessiyalarni bazada saqlashga o'tkazing.
- "Barcha qurilmalardan chiqish" imkoniyatini qo'shing.
- Massaviy tayinlash zaifligini toping va DTO bilan tuzating.
- Poyga shartini
FOR UPDATEbilan tuzating. - Barcha xavfsizlik sarlavhalarini qo'shing va
curlbilan tekshiring.
Xulosa #
- CSRF - brauzer cookie ni avtomatik yuborishidan foydalanadi.
- Himoya: token + SameSite + Origin tekshiruvi.
- SSRF - serverni ichki tarmoqqa so'rov yuborishga majburlaydi.
- SSRF ga qarshi: sxema, port va IP tekshiruvi; tarmoq darajasida bloklash.
- DNS qayta bog'lash - IP ni bir marta yeching va shu IP ga ulaning.
- Sessiya hujumlari: o'g'irlash, fiksatsiya, bashorat qilish.
- Kirishdan keyin
session_regenerate_id(true)majburiy. - Tokenni
localStorageda saqlamang. - Massaviy tayinlash - DTO bilan ruxsat etilgan maydonlarni cheklang.
- Biznes mantiq zaifliklarini skanerlar topa olmaydi.
- Poyga shartlari uchun
FOR UPDATEqulflash. - CORS - brauzer cheklovi, himoya emas.
Keyingi bo'limda ma'lumot xavfsizligini ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.