PHP / CSRF хамгаалалт

CSRF хамгаалалт

CSRF (Cross-Site Request Forgery) — буруу орчуулбал "өөр сайтаас хүсэлт хуурамчлах" гэсэн утгатай. Хэрэглэгч таны сайтад нэвтэрсэн байхад хортой вэбсайт тухайн хэрэглэгчийн нэрийн өмнөөс таны серверт хүсэлт явуулдаг. Хамгаалалт нь энгийн боловч маш чухал — нэвтрэлт шаардагдах аль ч маягтад заавал хэрэглэнэ.

CSRF халдлага хэрхэн ажилладаг вэ?

код
1. Хэрэглэгч bank.mn сайтад нэвтэрнэ (session үүснэ)
2. Хэрэглэгч хортой сайт руу орно (жишээ нь: free-prize.mn)
3. Хортой сайт хэрэглэгчийн хөтчөөр bank.mn руу хүсэлт явуулна:
   <img src="https://bank.mn/transfer?to=hacker&amount=1000000">
4. Хөтөч bank.mn-ийн session cookie-г автоматаар дагуулна
5. Банк "хэрэглэгч өөрөө явуулсан" гэж үзэн гүйцэтгэнэ

Хамгаалалтгүй маягт иймэрхүү харагдана:

php
<?php
// ❌ CSRF хамгаалалтгүй — аюултай!
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $дүн = (float) $_POST['дүн'];
    $данс = $_POST['данс'];
    // Хүсэлт хаанаас ирснийг шалгахгүй — халдлагад нэрвэгдэнэ
    гүйлгээ_хийх($данс, $дүн);
}
?>
<form method="POST" action="/transfer">
    <input name="данс" value="">
    <input name="дүн" value="">
    <button>Шилжүүлэх</button>
</form>

CSRF token — үндсэн хамгаалалт

Санамсаргүй, тааж болшгүй token үүсгэж маягтад нуун оруулдаг. Сервер хүсэлт хүлээн авахад тухайн token-г шалгадаг:

php
<?php
session_start();

// Token үүсгэх функц
function csrf_token_үүсгэх(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Token шалгах функц
function csrf_шалгах(): void
{
    $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
        http_response_code(403);
        die("CSRF шалгалт амжилтгүй боллоо.");
    }
}

// Маягт харуулах
$token = csrf_token_үүсгэх();
?>
<form method="POST" action="/transfer">
    <!-- Token нуун оруулна — хортой сайт энийг мэдэхгүй -->
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
    <input type="text" name="данс" placeholder="Дансны дугаар">
    <input type="number" name="дүн" placeholder="Дүн">
    <button type="submit">Шилжүүлэх</button>
</form>
php
<?php
// POST хүсэлт хүлээн авах
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_шалгах();   // Эхлээд token шалгана

    // Token зөв бол үргэлжлүүлнэ
    $дүн = filter_input(INPUT_POST, 'дүн', FILTER_VALIDATE_FLOAT);
    $данс = htmlspecialchars($_POST['данс'] ?? '', ENT_QUOTES, 'UTF-8');

    if ($дүн && $данс) {
        гүйлгээ_хийх($данс, $дүн);
        echo "Гүйлгээ амжилттай!";
    }
}
?>

Дахин ашиглагдах CsrfGuard класс

Бодит төсөлд дахин дахин ашиглах боломжтой класс болгоно:

php
<?php
class CsrfGuard
{
    private const TOKEN_УРТ = 32;
    private const SESSION_ТҮЛХҮҮР = '_csrf';

    // Token авах эсвэл шинэ үүсгэх
    public static function token(): string
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        if (empty($_SESSION[self::SESSION_ТҮЛХҮҮР])) {
            $_SESSION[self::SESSION_ТҮЛХҮҮР] = bin2hex(
                random_bytes(self::TOKEN_УРТ)
            );
        }

        return $_SESSION[self::SESSION_ТҮЛХҮҮР];
    }

    // HTML input field буцаана
    public static function поле(): string
    {
        $token = htmlspecialchars(self::token(), ENT_QUOTES, 'UTF-8');
        return '<input type="hidden" name="csrf_token" value="' . $token . '">';
    }

    // POST хүсэлт шалгана — амжилтгүй бол 403 хариулна
    public static function шалга(): void
    {
        $оролт = $_POST['csrf_token']
            ?? $_SERVER['HTTP_X_CSRF_TOKEN']
            ?? '';

        if (!hash_equals(self::token(), $оролт)) {
            http_response_code(403);
            header('Content-Type: application/json');
            echo json_encode(['алдаа' => 'Хүсэлт баталгаажуулалт амжилтгүй.']);
            exit;
        }

        // Нэг удаа хэрэглэсний дараа шинэчлэх (optional)
        unset($_SESSION[self::SESSION_ТҮЛХҮҮР]);
    }
}
?>

Ашиглах нь маш хялбар болно:

php
<?php
session_start();
?>
<!-- Маягт дотор -->
<form method="POST" action="/нийтлэл-устгах">
    <?= CsrfGuard::поле() ?>
    <input type="hidden" name="id" value="42">
    <button type="submit">Устгах</button>
</form>

<?php
// Хүсэлт хүлээн авахад
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    CsrfGuard::шалга();   // Нэг мөрт шалгалт
    // Цааш аюулгүйгээр үргэлжлүүлнэ
}
?>

AJAX хүсэлтэд CSRF хамгаалалт

php
<?php
// Хуудсын эхэнд JavaScript-д token дамжуулна
session_start();
$csrf = CsrfGuard::token();
?>
<script>
    const CSRF_TOKEN = '<?= htmlspecialchars($csrf, ENT_QUOTES) ?>';

    // AJAX хүсэлт бүрт header дотор явуулна
    async function аюулгүй_хүсэлт(url, өгөгдөл) {
        const хариу = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': CSRF_TOKEN,      // Header дотор
            },
            body: JSON.stringify(өгөгдөл),
        });
        return хариу.json();
    }

    // Ашиглах
    аюулгүй_хүсэлт('/api/нийтлэл', { гарчиг: 'Шинэ нийтлэл' })
        .then(хариу => console.log(хариу));
</script>
php
<?php
// API route дотор header-ийн token шалгана
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // $_SERVER['HTTP_X_CSRF_TOKEN'] нь X-CSRF-Token header-ийг агуулна
    CsrfGuard::шалга();
    // ...
}
?>

Дараагийн хичээлд:

Тогтмол илэрхийлэл буюу Regular Expression (RegExp)-г судална. Имэйл, утасны дугаар, огноо зэрэг форматыг баталгаажуулах, мөр дотроос хэв загвар хайх, солих зэрэг хүчирхэг боловсруулалтыг үзнэ.