在處理用戶提交的富文本內容(評論、文章、聊天訊息)時,若直接輸出未經過濾的 HTML,極易導致 XSS (跨站腳本攻擊)、排版破壞、惡意腳本注入等安全風險。本文整理了 PHP 中使用正則表達式過濾 HTML 標籤、屬性、危險腳本的實用模式,並對比原生函數與現代最佳實踐。
$str = preg_replace('/\s+/', ' ', $str); // 合併多餘空白字符(含換行)
$str = preg_replace('/<[ ]+/si', '<', $str); // 移除標籤左尖括號後的空格
$str = preg_replace('/<!--.*?-->/si', '', $str); // 移除 HTML 註解
$str = preg_replace('/<(!.*?)>/si', '', $str); // 移除 DOCTYPE 宣告
$patterns = [
'/<(\/?html.*?)>/si',
'/<(\/?head.*?)>/si',
'/<(\/?meta.*?)>/si',
'/<(\/?body.*?)>/si',
'/<(\/?link.*?)>/si',
'/<(\/?form.*?)>/si',
];
$str = preg_replace($patterns, '', $str);
// 移除完整標籤對(含內容):script, style, iframe, object, applet, frameset, title
$dangerous_tags = ['script', 'style', 'iframe', 'object', 'applet', 'frameset', 'title', 'noscript'];
foreach ($dangerous_tags as $tag) {
$str = preg_replace("/<($tag.*?)>(.*?)<(\/$tag.*?)>/si", '', $str); // 含內容
$str = preg_replace("/<(\/?$tag.*?)>/si", '', $str); // 殘留單標籤
}
// 移除事件屬性:onclick, onload, onerror, onmouseover 等
$str = preg_replace('/\s(on[a-z]+)\s*=/si', ' data-\\1=', $str); // 改為 data-* 屬性失效化
// 移除 javascript:、vbscript: 協議
$str = preg_replace('/javascript:/si', 'javascript:', $str); // 大小寫混淆繞過
$str = preg_replace('/vbscript:/si', 'vbscript:', $str);
// 實體編碼攔截
$str = preg_replace('//si', '&#', $str);
黑名單永遠不完整,白名單才是正道。 只允許 p, br, strong, em, u, span, div, img, a, ul, ol, li, blockquote, code, pre, h1-h6 等安全標籤:
function html_whitelist_filter($html, $allowed_tags = 'p,br,strong,em,u,span,div,img,a,ul,ol,li,blockquote,code,pre,h1,h2,h3,h4,h5,h6') {
// 1. 先移除所有事件屬性與危險協議
$html = preg_replace('/\s(on\w+)\s*=/i', ' data-\\1=', $html);
$html = preg_replace('/(href|src)\s*=\s*["\']?\s*(javascript|vbscript|data):/i', '\\1="#blocked"', $html);
// 2. 使用 strip_tags 保留白名單(注意:strip_tags 不檢查屬性安全性)
$html = strip_tags($html, '<' . implode('><', explode(',', $allowed_tags)) . '>');
// 3. 進一步清理保留標籤的危險屬性(需配合 DOMDocument 或 HTMLPurifier)
return $html;
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
strip_tags() |
內建、速度快、語法簡單 | 無法過濾屬性(如 onclick、style);允許標籤時屬性全保留;HTML5 新標籤支援有限 |
極簡場景、完全信任的內容 |
自訂正則 preg_replace |
靈活、可精準控制、無依賴 | 正則難以完美解析巢狀/破損 HTML;效能隨內容增長下降;極易漏過繞過技巧(編碼、大小寫、換行混淆) | 簡單過濾、特定格式清理、學習用途 |
| HTMLPurifier (推薦) | 標準合規、白名單機制、防禦所有已知 XSS、支援 HTML5、可配置極細緻 | 體積大、效能較低(可配合快取)、需 Composer 安裝 | 所有生產環境富文本處理 |
DOMDocument + XPath |
正確解析 DOM 樹、可精準操作節點屬性、原生無依賴 | 對不合法 HTML 容錯差(需 libxml_use_internal_errors);API 複雜;大文件記憶體佔用高 |
結構化提取、特定屬性清理、不想引入重型庫 |
// Composer 安裝: composer require ezyang/htmlpurifier
require_once 'vendor/autoload.php';
$config = HTMLPurifier_Config::createDefault();
// 核心白名單設定
$config->set('HTML.Allowed', 'p,br,strong,em,u,span[style],div[style],img[src|alt|width|height],a[href|title|target],ul,ol,li,blockquote,code,pre,h1,h2,h3,h4,h5,h6');
// 允許的 CSS 樣式屬性(防止 CSS 注入)
$config->set('CSS.AllowedProperties', 'color,background-color,font-size,font-weight,text-align,text-decoration,margin,padding,border');
// 圖片限制同域或 HTTPS
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
$config->set('Attr.EnableID', true); // 允許 id 屬性
$purifier = new HTMLPurifier($config);
$safe_html = $purifier->purify($dirty_html);
<ScRiPt> — 需加 i 修飾符<img src=javascript:alert(1)> — 需先解碼實體再過濾<img/src='x'/onerror=alert(1)> — 正則難以全覆蓋<script><script>alert(1)</script></script> — 貪婪/非貪婪匹配失效<!--[if IE]><script>...</script><![endif]-->結論:手寫正則無法覆蓋所有邊界情況,生產環境務必使用 HTMLPurifier。
strip_tags + 基礎正則清理屬性。Content-Security-Policy: script-src 'self'; object-src 'none';