早期 PHP 專案常見 StrSwap 等自訂類別,在進入點統一清理 $_GET/$_POST,企圖一次解決 SQL 注入、XSS、指令注入等問題。這種「全域神奇清理」違反情境輸出編碼原則,不但無法完整防禦,反而引入資料損壞、誤判、維護困難。本文分析傳統做法缺失,建立現代分層防禦體系:輸入驗證、情境輸出編碼、內容安全政策 (CSP)、資料庫參數化。
<?php
// 原始碼典型問題
class StrSwap {
function StrReplace($str) {
$str = StripSlashes($str); // 1. 盲目 stripslashes,破壞合法反斜線
$str = str_replace(chr(92), '', $str); // 2. 移除所有反斜線,導致路徑、正則、JSON 失效
$str = str_replace(chr(47), '', $str); // 3. 移除斜線,破壞 URL、日期、分數
$str = str_replace("\r\n", "
", $str); // 4. 過早 HTML 化,資料庫存入
導致 JSON/API 輸出錯誤
$str = str_replace('<', '<', $str); // 5. 僅處理 < >,漏掉屬性注入、JS 事件、CSS 注入
$str = str_replace('>', '>', $str);
$str = str_replace(';', ';', $str); // 6. 全形替換破壞程式碼、CSV、語意
$str = str_replace('"', '“', $str); // 7. 破壞 JSON、HTML 屬性值、SQL 字串
$str = str_replace("'", '‘', $str); // 8. 破壞 SQL、JS 字串、人名(O'Connor)
$str = str_replace(" ", " ", $str); // 9. 破壞空白語意、排版、搜尋
$str = str_replace("/**/", " ", $str); // 10. 針對特定繞過,治標不治本
return trim($str);
}
// ... 自動將 $_GET/$_POST 轉為全域變數,極度危險(register_globals 風格)
}
?>
| 缺陷 | 後果 | 正確做法 |
|---|---|---|
| 全域清理 | 破壞合法資料(檔案路徑、程式碼、JSON、Base64、郵件內容) | 針對特定情境處理:資料庫用參數化、HTML 用 htmlspecialchars、JS 用 json_encode、Shell 用 escapeshellarg |
| 過早 HTML 編碼 | 資料庫存入 <br>,導致 API 回傳雙重編碼、搜尋失效、編輯器顯示實體碼 | 資料庫儲存原始資料,輸出到 HTML 時才編碼 |
| 字元黑名單 | 永遠不完整(編碼繞過、Unicode 同形字、CSS/JS 多樣化注入向量) | 白名單驗證 + 情境編碼 |
| 自動全域變數 | 變數覆蓋攻擊、命名空間汙染、難以追蹤資料流 | 明確使用 $_GET/$_POST/$_REQUEST,或框架 Request 物件 |
| 缺乏編碼感知 | 多位元組字元(UTF-8)處理錯誤,導致截斷、亂碼 | 使用 mb_* 函數、htmlspecialchars($str, ENT_QUOTES, 'UTF-8') |
在控制器/表單請求層,針對業務規則驗證:
<?php
// 使用 Laravel 風格表單請求或獨立驗證器
class StorePostRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200', 'regex:/^[\p{L}\p{N}\s\-_(),.!?]+/u'],
'content' => ['required', 'string', 'max:50000'],
'email' => ['required', 'email:rfc,dns'],
'age' => ['required', 'integer', 'between:1,120'],
'tags' => ['array', 'max:10'],
'tags.*' => ['string', 'max:30', 'alpha_dash'],
];
}
public function messages(): array
{
return [
'title.regex' => '標題包含非法字元',
];
}
}
// 控制器
public function store(StorePostRequest $request)
{
$validated = $request->validated(); // 只含通過驗證的資料
// $validated['title'] 仍是原始字串,未經 HTML 編碼
}
?>
原則:驗證「是否符合預期格式」,而非「移除危險字元」。拒絕不合法輸入,而非試圖清理。
<?php
// 唯一正確防禦 SQL 注入的方式
$stmt = $pdo->prepare('INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)');
$stmt->execute([$validated['title'], $validated['content'], $userId]);
// 或具名參數
$stmt = $pdo->prepare('INSERT INTO posts (title, content, user_id) VALUES (:title, :content, :uid)');
$stmt->execute(['title' => $validated['title'], 'content' => $validated['content'], 'uid' => $userId]);
?>
參數化查詢將資料與指令分離,資料庫引擎保證資料永遠不會被解釋為 SQL。
| 輸出情境 | 編碼函數 | 範例 |
|---|---|---|
| HTML 內文/屬性值 | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <div>= h($title) ?></div>, <input value="= h($name) ?>"> |
| HTML 屬性值(單引號) | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <input value='= h($name) ?>'> |
| JavaScript 字串/JSON | json_encode($str, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) | <script>const name = = json_encode($name, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) ?>;</script> |
| CSS 值 | 白名單驗證或 cssesc() (paragonie/csp-builder) | style="color: = validateColor($color) ?>;" |
| URL 參數 | rawurlencode($str) | <a href="/search?q== rawurlencode($q) ?>"> |
| Shell 指令參數 | escapeshellarg($str) | exec('convert ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)) |
| SQL 字串字面值(極少手寫) | $pdo->quote($str) | 僅用於無法參數化的極罕見情況 |
<?php
function h(string $str, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, string $encoding = 'UTF-8'): string
{
return htmlspecialchars($str, $flags, $encoding);
}
function js(string $str): string
{
return json_encode($str, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
}
function url(string $str): string
{
return rawurlencode($str);
}
?>
透過 HTTP 標頭限制瀏覽器載入/執行資源來源,即使發生 XSS 也能限制傷害:
<?php
// 中介層或入口檔案
$nonce = bin2hex(random_bytes(16));
header("Content-Security-Policy: " . implode('; ', [
"default-src 'self'",
"script-src 'self' 'nonce-$nonce' 'strict-dynamic' https://cdn.jsdelivr.net",
"style-src 'self' 'nonce-$nonce' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]));
// 將 nonce 傳給視圖
$GLOBALS['csp_nonce'] = $nonce;
?>
視圖中:<script nonce="= $csp_nonce ?>">...</script>
StrSwap、auto_prepend_file 等全域攔截。echo $var 替換為 echo h($var)(HTML 內文/屬性)、echo js($var)(JS 內容)。Content-Security-Policy-Report-Only 開始收集違規報告,逐步收緊。html_entity_decode)還原原始內容。mysqli_real_escape_string 能防 SQL 注入?strip_tags() 能防 XSS?<img src=x onerror=alert(1)>)、CSS 注入、DOM 型 XSS。htmlspecialchars 才是正確方案。v-html/dangerouslySetInnerHTML、屬性綁定、URL、CSS、JS 互動點仍需手動處理。資安沒有銀彈,只有分層防禦。拋棄「全域神奇清理」迷思,擁抱輸入驗證 + 參數化查詢 + 情境輸出編碼 + CSP的現代體系。每一層都簡單、明確、可測試,組合起來才能構建真正韌性的應用程式。記住:資料本身無毒,毒在於被錯誤解釋的情境。