直接輸出用戶提交的內容會導致 XSS (跨站腳本攻擊)、版面破壞、資訊洩露。防禦核心是輸出端編碼與白名單過濾。
<?php
function safe_text_output($input) {
return htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// 使用
echo "" . safe_text_output($_POST['comment']) . "";
// 輸出範例
// 輸入: 粗體
// 輸出: <script>alert(1)</script><b>粗體</b>
// 瀏覽器顯示為純文字,不執行、不渲染標籤
適用:純文字欄位、搜尋關鍵字、JSON API 回傳、日誌記錄。
允許用戶使用安全子集:[b]粗體[/b]、[i]斜體[/i]、[url=...]連結[/url]。
<?php
function format_custom_markup($text) {
// 1. 完全轉義
$text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// 2. 簡單標籤替換 (str_replace 足夠)
$text = str_replace('[b]', '', $text);
$text = str_replace('[/b]', '', $text);
$text = str_replace('[i]', '', $text);
$text = str_replace('[/i]', '', $text);
$text = str_replace('[u]', '', $text);
$text = str_replace('[/u]', '', $text);
// 3. 複雜標籤用正則 (連結、圖片、代碼塊)
// [url=https://example.com]連結文字[/url]
$text = preg_replace(
'/\[url=([^\]]+)\](.*?)\[\/url\]/is',
'$2',
$text
);
// [img]https://example.com/image.png[/img]
$text = preg_replace(
'/\[img\](https?:\/\/[^\s\]]+)\[\/img\]/i',
'
',
$text
);
// [code]程式碼[/code]
$text = preg_replace(
'/\[code\](.*?)\[\/code\]/is',
'$1
',
$text
);
// 4. 換行轉
(最後處理)
$text = nl2br($text);
return $text;
}
簡單替換無法處理 [b][i]文字[/b][/i] 錯誤巢狀。建議:
若必須允許用戶輸入真實 HTML (如富文本編輯器),必須用 HTMLPurifier,手寫正則極難完備。
<?php
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
// 允許的標籤與屬性
$config->set('HTML.Allowed', 'p,b,strong,i,em,u,strike,a[href|title|target|rel],img[src|alt|width|height],br,ul,ol,li,blockquote,pre,code,h1,h2,h3,h4,h5,h6');
// 允許的 CSS 樣式 (可選)
$config->set('CSS.AllowedProperties', 'font-weight,font-style,text-decoration,color,background-color,text-align');
// 目標輸出編碼
$config->set('Core.Encoding', 'UTF-8');
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html_from_user);
// $clean_html 保證安全、標準、無 XSS
| 錯誤做法 | 風險 | 正確做法 |
|---|---|---|
只過濾 <script> | 繞過極易 (事件屬性、CSS、編碼混淆) | 完整轉義或白名單 |
strip_tags() 允許部分標籤 | 屬性未過濾 (onclick、style、onload) | HTMLPurifier |
| 儲存前過濾,輸出不處理 | 二次注入、語境不同 (HTML/JS/URL/CSS) | 輸出端編碼 (儲存原文) |
| JS 端過濾 | 繞過、禁用 JS 失效 | 伺服器端處理 |
正則匹配 <.*?> 移除標籤 | 貪婪/非貪婪失效、屬性值含 > 截斷 | 解析器/HTMLPurifier |
同一資料在不同語境需不同編碼:
| 語境 | 編碼函式 | 範例 |
|---|---|---|
| HTML 內文/屬性值 | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <div>= h($str) ?></div> |
| HTML 屬性 (單引號) | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <input value='= h($str) ?>'> |
| JavaScript 字串 | json_encode($str) (含引號) | <script>var x = = json_encode($str) ?>;</script> |
| URL 參數值 | urlencode($str) | <a href="?q== urlencode($str) ?>"> |
| CSS 值 | 專用編碼 (極少用) | <div style="color: = css_escape($str) ?>;"> |
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; 作為最後防線。HttpOnly、Secure、SameSite=Strict/Lax。安全顯示用戶內容的鐵律:儲存原文、輸出編碼、語境感知、白名單過濾。簡單場景用 htmlspecialchars(),受控格式用自訂標籤/Markdown 解析器,複雜富文本務必用 HTMLPurifier。切記:過濾在輸出端,而非輸入端。