17-bo‘lim

Xavfsizlik

XSS, CSRF, parol xeshlash, sirlarni saqlash va xavfsiz sarlavhalar - amaliy himoya qatlamlari.

🕑 13 daqiqa o‘qish 📄 922 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. XSS - begona kodni sahifaga kiritish
  2. CSRF - begona saytdan yuborilgan forma
  3. Parollarni xeshlash
  4. Sirlarni saqlash
  5. Xavfsizlik sarlavhalari
  6. Kirish tizimi - to'liq namuna
  7. Xulosa

Xavfsizlik - alohida bo'lim emas, har bir qatorda qilinadigan ish. Lekin eng ko'p uchraydigan hujumlarni bir joyda ko'rib chiqish foydali.

Bu bo'lim faqat himoya haqida: har bir hujumni tushuntirib, undan qanday saqlanishni ko'rsatamiz.

XSS - begona kodni sahifaga kiritish #

PHP
// Har bir misol qat'iy turlar rejimida ishlaydi.
function e(?string $matn): string
{
    return htmlspecialchars($matn ?? "", ENT_QUOTES, "UTF-8");
}
PHP
$xavfli = '<script>alert("xss")</script>';
// Atribut APOSTROF bilan yozilgan hujum
$atributHujumi = "' onmouseover='alert(1)";

echo "1. Ekranlanmagan (XAVFLI):\n";
echo "   <p>{$xavfli}</p>\n";

echo "\n2. htmlspecialchars bilan:\n";
echo "   <p>" . e($xavfli) . "</p>\n";

echo "\n3. Atribut apostrofda - ENT_COMPAT YETARLI EMAS:\n";
echo "   ENT_COMPAT:  <input value='"
   . htmlspecialchars($atributHujumi, ENT_COMPAT, "UTF-8") . "'>\n";
echo "   ENT_QUOTES:  <input value='" . e($atributHujumi) . "'>\n";

echo "\n";
echo "Birinchi qatorda apostrof EKRANLANMAGAN - hujumchi\n";
echo "atributdan chiqib, onmouseover qo'sha oldi.\n";
echo "Ikkinchi qatorda esa u &#039; ga aylandi.\n";
Natija
1. Ekranlanmagan (XAVFLI):
   <p><script>alert("xss")</script></p>

2. htmlspecialchars bilan:
   <p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>

3. Atribut apostrofda - ENT_COMPAT YETARLI EMAS:
   ENT_COMPAT:  <input value='' onmouseover='alert(1)'>
   ENT_QUOTES:  <input value='&#039; onmouseover=&#039;alert(1)'>

Birinchi qatorda apostrof EKRANLANMAGAN - hujumchi
atributdan chiqib, onmouseover qo'sha oldi.
Ikkinchi qatorda esa u &#039; ga aylandi.
Ekranlash chiqish joyiga bog'liq

htmlspecialchars faqat HTML matni uchun. Boshqa kontekstlarda u yetarli emas yoki noto'g'ri:

Qayerga chiqadiTo'g'ri usul
HTML matnihtmlspecialchars($x, ENT_QUOTES, "UTF-8")
HTML atributiXuddi shu + atribut qo'shtirnoqda bo'lsin
URL parametriurlencode($x) yoki rawurlencode($x)
JavaScript ichidajson_encode($x, JSON_HEX_TAG | JSON_HEX_AMP)
CSS ichidaUmuman foydalanuvchi ma'lumotini qo'ymang
SQLTayyorlangan so'rov (16-bo'lim)

Eng xavfli chalkashlik - HTML uchun ekranlangan matnni JavaScript ichiga qo'yish:

PHP
<script>
    var ism = "<?= e($ism) ?>";     // YETARLI EMAS
</script>

Foydalanuvchi </script><script>alert(1) yozsa, htmlspecialchars < ni ekranlaydi va hujum ishlamaydi. Lekin \ va yangi qator kabi belgilar boshqa muammolar tug'diradi.

To'g'ri usul:

PHP
<script>
    var ism = <?= json_encode($ism, JSON_HEX_TAG | JSON_HEX_AMP
                                  | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
</script>

json_encode qo'shtirnoqlarni ham o'zi qo'yadi.

Eng yaxshi yechim esa - ma'lumotni data- atributiga qo'yish va JavaScript da uni o'qish. Shunda kod va ma'lumot umuman aralashmaydi.

CSRF - begona saytdan yuborilgan forma #

PHP
// CSRF tokeni: sessiyada saqlanadi, formaga qo'yiladi, qaytganda solishtiriladi
final class CsrfHimoya
{
    private array $sessiya = [];

    public function token(): string
    {
        if (!isset($this->sessiya["csrf"])) {
            // Haqiqiy kodda: bin2hex(random_bytes(32))
            $this->sessiya["csrf"] = str_repeat("a1b2", 8);
        }
        return $this->sessiya["csrf"];
    }

    public function maydon(): string
    {
        return '<input type="hidden" name="_token" value="' . $this->token() . '">';
    }

    public function tekshir(?string $kelgan): bool
    {
        $saqlangan = $this->sessiya["csrf"] ?? null;
        if ($saqlangan === null || $kelgan === null) {
            return false;
        }
        // hash_equals - VAQT hujumidan himoya
        return hash_equals($saqlangan, $kelgan);
    }
}

$csrf = new CsrfHimoya();

echo "Formaga qo'yiladigan maydon:\n";
echo "  " . $csrf->maydon() . "\n";

echo "\nTekshiruv natijalari:\n";
$sinovlar = [
    "to'g'ri token"     => $csrf->token(),
    "noto'g'ri token"   => "boshqa-token-qiymati",
    "bo'sh"             => null,
    "qisqartirilgan"    => substr($csrf->token(), 0, 10),
];

foreach ($sinovlar as $nom => $qiymat) {
    printf("  %-18s %s\n", $nom, $csrf->tekshir($qiymat) ? "QABUL" : "rad etildi");
}
Natija
Formaga qo'yiladigan maydon:
  <input type="hidden" name="_token" value="a1b2a1b2a1b2a1b2a1b2a1b2a1b2a1b2">

Tekshiruv natijalari:
  to'g'ri token      QABUL
  noto'g'ri token    rad etildi
  bo'sh              rad etildi
  qisqartirilgan     rad etildi
=== emas, hash_equals ishlating

Oddiy === solishtiruvi birinchi farq topilganda darhol to'xtaydi. Ya'ni ikki satr qanchalik o'xshash bo'lsa, solishtirish shunchalik uzoq davom etadi.

Hujumchi bu vaqt farqini o'lchab, tokenni belgima-belgi topib olishi mumkin. Bu vaqt hujumi (timing attack) deb ataladi.

hash_equals esa har doim bir xil vaqt sarflaydi - satrlar qanchalik o'xshash bo'lishidan qat'i nazar.

Uni qayerda ishlatish kerak:

Nima solishtirilyaptiUsul
CSRF tokenihash_equals
API kalitihash_equals
Sessiya identifikatorihash_equals
Parol xeshipassword_verify (o'zi qiladi)
Oddiy matn=== yetarli

CSRF himoyasi to'liq bo'lishi uchun uch narsa kerak:

  1. Token - forma va sessiyada mos kelsin.
  2. SameSite=Lax cookie (10-bo'lim).
  3. O'zgartiruvchi amallar faqat POST orqali (8-bo'lim).

Uchtasi birga ishlaganda CSRF amalda imkonsiz bo'ladi.

Parollarni xeshlash #

PHP
$parol = "MeningKuchliParolim2024";

// 1. Xeshlash - har safar BOSHQA natija beradi
$xesh1 = password_hash($parol, PASSWORD_DEFAULT);
$xesh2 = password_hash($parol, PASSWORD_DEFAULT);

echo "Xesh uzunligi:      " . strlen($xesh1) . " belgi\n";
echo "Ikki xesh bir xilmi: " . ($xesh1 === $xesh2 ? "ha" : "yo'q") . "\n";
echo "(Har xeshda tasodifiy TUZ bor - shuning uchun har xil)\n";

echo "\n";
// 2. Tekshirish
printf("To'g'ri parol:    %s\n", password_verify($parol, $xesh1) ? "QABUL" : "rad");
printf("Noto'g'ri parol:  %s\n", password_verify("boshqaParol", $xesh1) ? "QABUL" : "rad");
printf("Ikkinchi xesh:    %s\n", password_verify($parol, $xesh2) ? "QABUL" : "rad");

echo "\n";
// 3. Xesh ichida nima bor
$qismlar = explode("$", $xesh1);
echo "Xesh tuzilishi:\n";
printf("  algoritm:  %s\n", $qismlar[1]);
printf("  narx:      %s\n", $qismlar[2]);
printf("  tuz+xesh:  %d belgi\n", strlen($qismlar[3]));

echo "\n";
// 4. Algoritm eskirganini tekshirish
$eskiXesh = password_hash($parol, PASSWORD_BCRYPT, ["cost" => 4]);
$yangilash = password_needs_rehash($eskiXesh, PASSWORD_DEFAULT);
echo "Eski xesh yangilanishi kerakmi: " . ($yangilash ? "ha" : "yo'q") . "\n";

$info = password_get_info($xesh1);
echo "Joriy xesh algoritmi: " . $info["algoName"] . "\n";
Natija
Xesh uzunligi:      60 belgi
Ikki xesh bir xilmi: yo'q
(Har xeshda tasodifiy TUZ bor - shuning uchun har xil)

To'g'ri parol:    QABUL
Noto'g'ri parol:  rad
Ikkinchi xesh:    QABUL

Xesh tuzilishi:
  algoritm:  2y
  narx:      10
  tuz+xesh:  53 belgi

Eski xesh yangilanishi kerakmi: ha
Joriy xesh algoritmi: bcrypt
Parol uchun md5, sha1 yoki sha256 ishlatmang

Bu funksiyalar tez ishlash uchun yaratilgan - va aynan shu ularni parol uchun yaroqsiz qiladi.

Zamonaviy videokarta soniyasiga milliardlab SHA-256 xeshini hisoblaydi. Ya'ni oddiy parol bir necha soniyada topiladi.

password_hash esa ataylab sekin:

FunksiyaSoniyada nechta urinish
md5Milliardlab
sha256Milliardlab
password_hash (bcrypt)O'nlab

Uchta qoida:

1. PASSWORD_DEFAULT ishlating. U vaqt o'tishi bilan eng yaxshi algoritmga o'zgaradi - kodingizni o'zgartirmasdan.

2. Ustunni VARCHAR(255) qiling. Bugun bcrypt 60 belgi, lekin kelajakdagi algoritm uzunroq bo'lishi mumkin.

3. Kirishda password_needs_rehash ni tekshiring:

PHP
if (password_verify($parol, $xesh)) {
    if (password_needs_rehash($xesh, PASSWORD_DEFAULT)) {
        $yangiXesh = password_hash($parol, PASSWORD_DEFAULT);
        // bazaga yozing
    }
}

Shunda foydalanuvchilar tizimga kirgani sari ularning xeshlari avtomatik yangilanadi.

Va tuzni o'zingiz qo'shmang - password_hash uni o'zi yaratadi va xesh ichiga joylaydi.

Asosiy hujumlar va ularga qarshi himoya SQL in'eksiya Foydalanuvchi qiymati SQL buyrug'iga aylanadi Himoya: $pdo->prepare(...) + execute([$q]) XSS Begona JavaScript sahifada bajariladi Himoya: htmlspecialchars($x, ENT_QUOTES, "UTF-8") CSRF Begona sayt sizning nomingizdan forma yuboradi Himoya: token + hash_equals + SameSite=Lax Parol o'g'irlash Baza sizib chiqsa - parollar ochiladi Himoya: password_hash / password_verify Umumiy tamoyil Har bir hujum - MA'LUMOT ning KOD ga aylanib qolishi. Yechim har doim bitta: ikkalasini alohida uzating va chegarada ekranlang.
To'rt hujum - bitta tamoyil: ma'lumot hech qachon kodga aylanmasin

Sirlarni saqlash #

PHP
<?php
// config/sozlama.php - bu fayl git ga QO'SHILMAYDI
declare(strict_types=1);

return [
    "baza" => [
        "dsn"    => getenv("DB_DSN") ?: "mysql:host=localhost;dbname=dokon;charset=utf8mb4",
        "user"   => getenv("DB_USER") ?: "root",
        "parol"  => getenv("DB_PASSWORD") ?: "",
    ],
    "pochta" => [
        "kalit" => getenv("MAIL_KEY") ?: "",
    ],
];
Parolni kodga yozmang va git ga qo'shmang

Eng ko'p uchraydigan jiddiy xato - .env yoki sozlama faylini git omboriga qo'shish.

Bir marta qo'shilgan sir tarixda abadiy qoladi. Uni keyinchalik o'chirsangiz ham, eski kommitlardan o'qib olish mumkin.

Majburiy .gitignore qatorlari:

KOD
.env
.env.*
!.env.example
config/sozlama.php
vendor/

Va .env.example faylini qiymatlarsiz qo'shing:

KOD
DB_DSN=mysql:host=localhost;dbname=NOMI;charset=utf8mb4
DB_USER=
DB_PASSWORD=

Shunda yangi dasturchi nima kerakligini biladi, lekin haqiqiy parollar omborga tushmaydi.

Agar sir allaqachon git ga tushgan bo'lsa:

QadamAmal
1Darhol parolni almashtiring
2Kalitlarni bekor qiling
3Faqat keyin tarixni tozalashni o'ylang

Birinchi qadam eng muhimi. Tarixni tozalash sirni allaqachon ko'rgan odamdan himoya qilmaydi.

14-bo'limda aytilgan qoidani ham unutmang: config/ papkasi veb ildizidan tashqarida bo'lsin.

Xavfsizlik sarlavhalari #

PHP
// Har bir javobga qo'yiladigan sarlavhalar
$sarlavhalar = [
    "Content-Security-Policy" => [
        "default-src 'self'",
        "Begona manbadan skript yuklanmasin - XSS ta'sirini keskin kamaytiradi",
    ],
    "X-Content-Type-Options" => [
        "nosniff",
        "Brauzer fayl turini o'zi taxmin qilmasin",
    ],
    "X-Frame-Options" => [
        "DENY",
        "Sahifa begona iframe ichiga joylanmasin (clickjacking)",
    ],
    "Referrer-Policy" => [
        "strict-origin-when-cross-origin",
        "Begona saytga to'liq URL yuborilmasin",
    ],
    "Strict-Transport-Security" => [
        "max-age=31536000; includeSubDomains",
        "Brauzer faqat HTTPS orqali ulansin",
    ],
];

foreach ($sarlavhalar as $nom => [$qiymat, $izoh]) {
    echo "{$nom}:\n";
    echo "  qiymat: {$qiymat}\n";
    echo "  nima uchun: {$izoh}\n\n";
}

echo "PHP da qo'yish:\n";
echo "  header(\"X-Frame-Options: DENY\");\n";
echo "  (yoki veb-server sozlamasida - bu afzalroq)\n";
Natija
Content-Security-Policy:
  qiymat: default-src 'self'
  nima uchun: Begona manbadan skript yuklanmasin - XSS ta'sirini keskin kamaytiradi

X-Content-Type-Options:
  qiymat: nosniff
  nima uchun: Brauzer fayl turini o'zi taxmin qilmasin

X-Frame-Options:
  qiymat: DENY
  nima uchun: Sahifa begona iframe ichiga joylanmasin (clickjacking)

Referrer-Policy:
  qiymat: strict-origin-when-cross-origin
  nima uchun: Begona saytga to'liq URL yuborilmasin

Strict-Transport-Security:
  qiymat: max-age=31536000; includeSubDomains
  nima uchun: Brauzer faqat HTTPS orqali ulansin

PHP da qo'yish:
  header("X-Frame-Options: DENY");
  (yoki veb-server sozlamasida - bu afzalroq)

Kirish tizimi - to'liq namuna #

PHP
<?php
declare(strict_types=1);

function kirishgaUrinish(PDO $pdo, string $email, string $parol): ?array
{
    // 1. Foydalanuvchini topamiz
    $stmt = $pdo->prepare("SELECT id, email, parol_xesh, rol FROM foydalanuvchilar WHERE email = ?");
    $stmt->execute([$email]);
    $f = $stmt->fetch();

    // 2. MUHIM: foydalanuvchi topilmasa ham xeshni tekshiramiz
    //    Aks holda javob vaqti email mavjudligini oshkor qiladi
    $xesh = $f["parol_xesh"] ?? '$2y$12$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

    if (!password_verify($parol, $xesh) || $f === false) {
        return null;        // bir xil xabar: "email yoki parol noto'g'ri"
    }

    // 3. Xesh eskirgan bo'lsa - yangilaymiz
    if (password_needs_rehash($f["parol_xesh"], PASSWORD_DEFAULT)) {
        $yangi = password_hash($parol, PASSWORD_DEFAULT);
        $pdo->prepare("UPDATE foydalanuvchilar SET parol_xesh = ? WHERE id = ?")
            ->execute([$yangi, $f["id"]]);
    }

    // 4. Sessiya identifikatorini YANGILAYMIZ (10-bo'lim)
    session_regenerate_id(true);
    $_SESSION["user_id"] = (int) $f["id"];
    $_SESSION["kirgan_vaqt"] = time();

    unset($f["parol_xesh"]);
    return $f;
}
Xato xabari hech qachon tafsilot bermasin

Yuqoridagi kodda ikki nozik detal bor.

1. Bir xil xabar. "Bunday email yo'q" va "Parol noto'g'ri" xabarlari hujumchiga qaysi emaillar ro'yxatdan o'tganini aytadi. Har doim bitta umumiy xabar bering: "Email yoki parol noto'g'ri".

2. Bir xil vaqt. Foydalanuvchi topilmasa, kod password_verify ni baribir chaqiradi. Aks holda javob sezilarli darajada tezroq qaytadi va bu mavjudlikni oshkor qiladi.

Bundan tashqari kirish tizimida yana uch narsa kerak:

HimoyaNima uchun
Urinishlar chegarasiParolni tanlab topishga qarshi
Kechikish oshirishHar xato urinishdan keyin kutish vaqtini oshiring
Ikki bosqichli tasdiqlashParol o'g'irlansa ham hisob himoyalanadi

Parolni tiklash havolasi uchun ham qoidalar bor: tasodifiy token (random_bytes(32)), qisqa muddat (1 soat) va bir marta ishlatilgach bekor qilish.

Amaliy topshiriq
  1. <script> tegini ekranlamasdan chiqaring va brauzerda oching.
  2. htmlspecialchars qo'shing va farqni ko'ring.
  3. ENT_QUOTES siz atribut ichida sinab ko'ring.
  4. CSRF tokeni yaratuvchi sinf yozing.
  5. === o'rniga hash_equals ishlating.
  6. Bir xil parolni ikki marta password_hash qiling - natijalar bir xilmi?
  7. password_verify bilan ikkalasini ham tekshiring.
  8. md5 bilan xeshlab, natija uzunligini solishtiring.
  9. password_needs_rehash ni eski cost bilan sinang.
  10. .gitignore ga .env ni qo'shing va .env.example yarating.

Xulosa #

  • Har bir hujumning mohiyati bitta: ma'lumot kodga aylanib qoladi.
  • Ekranlash chiqish joyiga bog'liq: HTML, atribut, URL va JavaScript uchun har xil.
  • HTML uchun htmlspecialchars($x, ENT_QUOTES, "UTF-8") - ENT_QUOTES shart.
  • JavaScript ichiga ma'lumotni json_encode bilan qo'ying yoki data- atributdan o'qing.
  • CSRF himoyasi uch qismdan iborat: token, SameSite=Lax, POST.
  • Token va kalitlarni hash_equals bilan solishtiring - === vaqt hujumiga ochiq.
  • Parol uchun md5/sha256 yaroqsiz - ular ataylab tez.
  • password_hash(PASSWORD_DEFAULT) ishlating; tuzni o'zingiz qo'shmang.
  • Parol ustunini VARCHAR(255) qiling va password_needs_rehash ni tekshiring.
  • Kirish xatosida bir xil xabar va bir xil vaqt bo'lsin.
  • Sirlarni .env da saqlang; uni hech qachon git ga qo'shmang.
  • Sir git ga tushgan bo'lsa - birinchi navbatda parolni almashtiring.

Keyingi bo'limda JSON va API yaratishni o'rganamiz.

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.