表單是 Web 應用與用戶互動的主要介面,也是攻擊面最廣的入口。完整的表單驗證包含:資料清理、格式驗證、業務規則、安全防護(XSS、CSRF、SQL Injection)、用戶體驗。本文建構系統性防護體系。
| 威脅 | 向量 | 後果 | 防護層級 |
|---|---|---|---|
| XSS | 輸入含腳本,輸出未編碼 | 竊取 Session、劫持帳號、惡意轉向 | 輸出編碼、CSP |
| CSRF | 偽造請求、用戶已登入 | 未授權操作(轉帳、改密、刪除) | Token 驗證、SameSite Cookie |
| SQL Injection | 輸入拼接 SQL | 資料洩露、竄改、刪除、提權 | 預處理語句 |
| Header Injection | 換行符注入 mail()、header() | 郵件濫發、回應分割、快取污染 | 移除換行、專用庫 |
| 檔案上傳攻擊 | 惡意檔案、雙重副檔名 | 遠端代碼執行、服務器佔領 | 類型驗證、重命名、隔離執行 |
<?php
function sanitizeInput(string $data, int $flags = ENT_QUOTES): string {
$data = trim($data);
$data = stripslashes($data); // 相容舊版 magic_quotes
$data = htmlspecialchars($data, $flags, 'UTF-8');
return $data;
}
// 針對不同輸出情境的清理
function sanitizeForHtml(string $data): string {
return htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function sanitizeForUrl(string $data): string {
return rawurlencode($data);
}
function sanitizeForJs(string $data): string {
return json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}
function sanitizeForShell(string $data): string {
return escapeshellarg($data);
}
<?php
class Validator {
public static function email(string $value): bool {
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
public static function url(string $value): bool {
return filter_var($value, FILTER_VALIDATE_URL,
FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED) !== false;
}
public static function ip(string $value): bool {
return filter_var($value, FILTER_VALIDATE_IP) !== false;
}
public static function int(string $value, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): bool {
$v = filter_var($value, FILTER_VALIDATE_INT, [
'options' => ['min_range' => $min, 'max_range' => $max]
]);
return $v !== false;
}
public static function regex(string $value, string $pattern): bool {
return preg_match($pattern, $value) === 1;
}
public static function inArray(string $value, array $allowed): bool {
return in_array($value, $allowed, true);
}
public static function length(string $value, int $min = 0, int $max = PHP_INT_MAX): bool {
$len = mb_strlen($value);
return $len >= $min && $len <= $max;
}
public static function notEmpty(string $value): bool {
return $value !== '' && $value !== null;
}
}
<?php
// csrf.php - 共用函數
function generateCsrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function validateCsrfToken(string $token): bool {
return isset($_SESSION['csrf_token']) &&
hash_equals($_SESSION['csrf_token'], $token);
}
// 針對 AJAX 的雙重提交 Cookie 模式
function generateCsrfCookie(): string {
$token = bin2hex(random_bytes(32));
setcookie('csrf_token', $token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => false, // JS 需讀取
'samesite' => 'Strict'
]);
$_SESSION['csrf_token'] = $token;
return $token;
}
function validateCsrfHeader(): bool {
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
$cookieToken = $_COOKIE['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';
return $headerToken !== '' &&
hash_equals($sessionToken, $headerToken) &&
hash_equals($sessionToken, $cookieToken);
}
?>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo generateCsrfToken(); ?>">
<!-- 欄位... -->
</form>
// JavaScript
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// 或從 Cookie 讀取
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
<?php
function requireCsrf(): void {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validateCsrfToken($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('CSRF Token 驗證失敗');
}
}
}
// API 路由
if (str_starts_with($_SERVER['REQUEST_URI'], '/api/')) {
if (!validateCsrfHeader()) {
http_response_code(403);
echo json_encode(['error' => 'CSRF 驗證失敗']);
exit;
}
}
?>
<?php
// PDO 預處理語句(唯一正確方式)
$stmt = $pdo->prepare('INSERT INTO users (name, email, role) VALUES (?, ?, ?)');
$stmt->execute([$name, $email, $role]);
// 命名參數
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute(['email' => $email, 'status' => 'active']);
// 禁止:字串拼接
// $sql = "SELECT * FROM users WHERE name = '$name'"; // 危險!
// LIKE 查詢參數化
$stmt = $pdo->prepare('SELECT * FROM products WHERE name LIKE ?');
$stmt->execute(["%{$keyword}%"]);
<?php
// process_form.php
require_once 'csrf.php';
require_once 'Validator.php';
session_start();
generateCsrfToken(); // 確保 Token 存在
$errors = [];
$clean = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. CSRF 驗證
if (!validateCsrfToken($_POST['csrf_token'] ?? '')) {
$errors['_form'] = '請求無效,請重新整理頁面再試';
} else {
// 2. 輸入清理與驗證
$name = sanitizeInput($_POST['name'] ?? '');
$email = sanitizeInput($_POST['email'] ?? '');
$age = (int)($_POST['age'] ?? 0);
if (!Validator::notEmpty($name)) $errors['name'] = '姓名必填';
elseif (!Validator::length($name, 1, 50)) $errors['name'] = '姓名長度 1-50 字';
if (!Validator::notEmpty($email)) $errors['email'] = 'Email 必填';
elseif (!Validator::email($email)) $errors['email'] = 'Email 格式錯誤';
if (!Validator::int((string)$age, 1, 120)) $errors['age'] = '年齡 1-120';
// 3. 業務規則(如 Email 唯一性)
if (empty($errors)) {
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) {
$errors['email'] = '此 Email 已註冊';
}
}
// 4. 成功處理
if (empty($errors)) {
$stmt = $pdo->prepare('INSERT INTO users (name, email, age, created_at) VALUES (?, ?, ?, NOW())');
$stmt->execute([$name, $email, $age]);
$success = true;
// PRG 重導向
header('Location: success.php');
exit;
}
}
}
// 5. 輸出表單(含錯誤、資料保持)
include 'form_template.php';
?>
<?php
function checkRateLimit(string $key, int $maxAttempts = 5, int $window = 300): bool {
$now = time();
$attempts = $_SESSION['rate_limit'][$key] ?? [];
// 清理過期記錄
$attempts = array_filter($attempts, fn($t) => $now - $t < $window);
if (count($attempts) >= $maxAttempts) {
return false;
}
$attempts[] = $now;
$_SESSION['rate_limit'][$key] = $attempts;
return true;
}
// 使用
if (!checkRateLimit('form_submit_' . $_SERVER['REMOTE_ADDR'], 10, 60)) {
$errors['_form'] = '提交過於頻繁,請稍後再試';
}
<?php
function logFormSubmission(array $data, array $errors, bool $success): void {
$log = [
'timestamp' => date('c'),
'ip' => $_SERVER['REMOTE_ADDR'],
'ua' => $_SERVER['HTTP_USER_AGENT'],
'url' => $_SERVER['REQUEST_URI'],
'method' => $_SERVER['REQUEST_METHOD'],
'success' => $success,
'errors' => $errors,
'data_keys' => array_keys($data), // 僅記錄欄位名,不記錄敏感值
];
// 移除敏感欄位
unset($log['data_keys'][array_search('password', $log['data_keys']) ?? '']);
unset($log['data_keys'][array_search('csrf_token', $log['data_keys']) ?? '']);
error_log('FORM_SUBMIT: ' . json_encode($log, JSON_UNESCAPED_UNICODE));
}
?>
sanitizeInput() 清理filter_var() / Validator 類別表單驗證不是單一函數,而是分層防護體系。從輸入清理、格式驗證、業務規則、CSRF/SQL/XSS 防護、速率限制、稽核日誌,每一層都不可或缺。系統性建構,才能在持續演進的威脅下守護應用安全。