PHP에서 CSRF 공격 및 중복 폼 제출 방지를 위한 토큰 기반 보안 처리

웹 애플리케이션 개발 시 사용자의 폼 데이터가 의도치 않게 중복 전송되거나, 외부 사이트로부터 위조된 요청이 발생하는 것을 막는 것은 보안 측면에서 매우 중요합니다. PHP에서는 세션 기반의 토큰(Token) 메커니즘을 활용해 이러한 문제를 효과적으로 해결할 수 있습니다.

토큰의 역할과 원리

토큰은 무작위로 생성되는 문자열로, 주로 사용자 세션에 저장되어 클라이언트와 서버 간 요청의 유효성을 검증하는 데 사용됩니다. 이 방식은 다음과 같은 두 가지 주요 목적에 사용됩니다:

  • 중복 제출 방지: 동일한 폼을 여러 번 제출하지 못하도록 차단
  • CSRF(사이트 간 요청 위조) 방어: 타 사이트에서 위조된 요청이 실행되지 않도록 차단

서버는 사용자가 폼 페이지를 요청할 때 고유한 토큰을 생성하고, 이를 세션에 저장한 후 HTML 폼 내 숨김 필드(<input type="hidden">)로 전달합니다. 사용자가 폼을 제출하면, 서버는 전달받은 토큰과 세션에 저장된 값이 일치하는지 확인합니다.

기본 구현 예제

다음은 PHP를 이용해 토큰 기반 검증을 구현한 간단한 예입니다.

<?php
session_start();

// 토큰 생성 함수
function generateToken() {
    $_SESSION['form_token'] = bin2hex(random_bytes(32));
}

// 토큰 검증 함수
function validateToken($submittedToken) {
    $isValid = isset($_SESSION['form_token']) && hash_equals($_SESSION['form_token'], $submittedToken);
    
    // 검증 후 토큰 재생성 (중복 제출 방지)
    generateToken();
    
    return $isValid;
}

// 세션에 토큰이 없으면 새로 생성
if (!isset($_SESSION['form_token'])) {
    generateToken();
}

// 폼 제출 처리
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (validateToken($_POST['token'] ?? '')) {
        echo '정상적으로 제출되었습니다: ' . htmlspecialchars($_POST['data']);
    } else {
        echo '무효한 요청입니다.';
    }
}
?>

<form method="post" action="">
    <input type="hidden" name="token" value="<?= $_SESSION['form_token'] ?>">
    <input type="text" name="data" value="기본값">
    <button type="submit">제출</button>
</form>
  

보안 강화된 토큰 생성기

더 안전한 토큰을 생성하기 위해 특수문자 조합과 해시 알고리즘을 활용할 수 있습니다. 아래는 사용자 정의 문자 집합을 기반으로 강력한 토큰을 생성하는 함수입니다.

<?php
function createSecureToken($length = 32) {
    $pool = array_merge(
        range('A', 'Z'), range('a', 'z'), range(0, 9),
        str_split('!@#$%^&*()_+-=[]{}|;:,./?')
    );
    $maxIndex = count($pool) - 1;
    $token = '';

    for ($i = 0; $i < $length; $i++) {
        $token .= $pool[random_int(0, $maxIndex)];
    }

    return hash('sha256', $token . uniqid('', true));
}
?>
  

폼 처리 분리 구조

실제 프로젝트에서는 폼 출력, 토큰 생성, 처리 로직을 분리하는 것이 좋습니다.

form_display.php

<?php
require_once 'security_helpers.php';
session_start();

$csrfToken = createSecureToken();
$_SESSION['csrf_token'] = $csrfToken;
?>

<form action="process_form.php" method="post">
    <input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
    <input type="text" name="username" placeholder="사용자명">
    <button type="submit">전송</button>
</form>
  

process_form.php

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    die('잘못된 접근입니다.');
}

$submittedToken = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';

if (!hash_equals($sessionToken, $submittedToken)) {
    http_response_code(403);
    die('접근이 거부되었습니다. 유효하지 않은 토큰입니다.');
}

// 토큰 사용 후 폐기
unset($_SESSION['csrf_token']);

echo '처리 완료: ' . htmlspecialchars($_POST['username']);
?>
  

추가 보안 수칙

토큰만으로는 충분하지 않으며, 다음 조치도 함께 적용해야 합니다:

  • XSS 공격 방지를 위해 출력 시 htmlspecialchars() 사용
  • 민감한 작업은 GET 대신 POST 요청으로 처리
  • HTTPS 환경에서 HttpOnlySecure 속성으로 쿠키 보호
  • Referer 헤더 검증 (보완적 수단)
  • CSRF 토큰은 각 폼마다 고유하게 유지

이러한 방법들은 보안성과 성능 사이의 균형을 고려하여 상황에 맞게 조합하여 사용해야 하며, 특히 금융, 인증 등 민감한 작업에는 반드시 다중 보안 장치를 적용하는 것이 권장됩니다.

태그: PHP CSRF 보안 토큰 세션

6월 24일 03:57에 게시됨