XSS (跨站腳本攻擊) 防護
核心原則
- 輸入過濾:不信任任何使用者輸入
- 輸出編碼:根據輸出語境 (HTML、JS、CSS、URL) 做對應編碼
- CSP 標頭:
Content-Security-Policy 限制腳本來源
- HttpOnly Cookie:防止 JS 竊取 Session
通用 XSS 清洗函式 (參考 CodeIgniter/Laravel)
function xss_clean(string $data): string {
// 1. 修正實體編碼
$data = str_replace(['&','<','>'], ['&','<','>'], $data);
$data = preg_replace('/(*\w+)[\x00-\x20]+;/u', '$1;', $data);
$data = preg_replace('/(*[0-9A-F]+);*/iu', '$1;', $data);
$data = html_entity_decode($data, ENT_COMPAT, 'UTF-8');
// 2. 移除危險屬性 (on*, xmlns)
$data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*>#iu', '$1>', $data);
// 3. 移除 javascript:/vbscript: 協定
$data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`"\']*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data);
$data = preg_replace('#([a-z]*)[\x00-\x20]*=([`"\']*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data);
// 4. 移除 expression/moz-binding (IE/舊 Firefox)
$data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`"\']*.*?expression[\x00-\x20]*\([^>]*>#i', '$1>', $data);
$data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`"\']*.*?behaviour[\x00-\x20]*\([^>]*>#i', '$1>', $data);
// 5. 移除命名空間標籤
$data = preg_replace('#?\w+:\w[^>]*>#i', '', $data);
// 6. 遞迴移除 script/iframe 等標籤
do {
$old = $data;
$data = preg_replace('#?(?:script|iframe|object|embed|applet|form|input|meta|link|style)[^>]*>#i', '', $data);
} while ($old !== $data);
return $data;
}
輸出編碼輔助函式
function h(string $str): string { // HTML 編碼
return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function js(string $str): string { // JavaScript 字串編碼
return json_encode($str); // 含引號
}
function url(string $str): string { // URL 參數編碼
return rawurlencode($str);
}
function attr(string $str): string { // HTML 屬性編碼
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
模板中正確使用
<!-- HTML 內容 -->
<div><?= h($user_content) ?></div>
<!-- HTML 屬性 -->
<input value="<?= attr($input_val) ?>" />
<!-- JavaScript 變數 -->
<script> var msg = <?= js($message) ?>; </script>
<!-- URL 參數 -->
<a href="/search?q=<?= url($keyword) ?>">搜尋</a>
<!-- CSS 值 -->
<div style="color: <?= h($color) ?>">
CSRF (跨站請求偽造) 防護
雙重 Cookie / Token 模式
<?php
// 產生 Token (Session + Cookie)
function csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 驗證
function csrf_verify(string $token): bool {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// 表單隱藏欄位
echo '';
// AJAX Header
// 前端: headers: {'X-CSRF-Token': token}
// 後端: $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_POST['_token'];
SameSite Cookie (現代瀏覽器首選)
// php.ini 或 runtime
ini_set('session.cookie_samesite', 'Strict'); // 或 'Lax'
// Laravel/框架通常自動處理
完整安全標頭建議
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");
框架內建防護 (勿重複造輪子)
| 框架 | XSS | CSRF |
| Laravel | Blade {{ }} 自動編碼 | @csrf / VerifyCsrfToken 中介層 |
| Symfony | Twig {{ }} 自動編碼 | CsrfTokenManager + Form 隱藏欄位 |
| CodeIgniter 4 | esc() / Security 類 | csrf_field() / Filter |
| Yii 2 | Html::encode() | Yii::$app->request->csrfToken |