用戶提交的評論、留言、富文本若直接輸出會導致 XSS 攻擊、樣式破壞、惡意跳轉。本文從基礎 strip_tags 到企業級 HTMLPurifier,系統梳理 PHP 端 HTML 過濾方案,並給出不同業務場景的選型建議。
| 方案 | 原理 | 安全性 | 性能 | 保留樣式 | 適用場景 |
|---|---|---|---|---|---|
strip_tags | 正則移除標籤 | ⭐ 低(屬性殘留、編碼繞過) | ⚡ 極快 | ❌ 全部移除 | 純文本輸入(用戶名、標籤) |
htmlspecialchars | 實體轉義 | ⭐⭐⭐⭐⭐ 高 | ⚡ 極快 | ❌ 顯示源碼 | 直接輸出到 HTML 文本節點 |
| 自定義允許列表 | 正則/解析器保留特定標籤 | ⭐⭐ 中(難處理嵌套、事件屬性) | ⚡⚡ 中 | ✅ 部分 | 簡單富文本(公告、備註) |
HTMLPurifier | 詞法分析+白名單+安全過濾 | ⭐⭐⭐⭐⭐ 極高 | 🐢 慢(可緩存) | ✅ 完全可控 | 富文本編輯器、CMS、論壇 |
| 前端過濾 | DOMPurify 等 | ⚠️ 輔助,不可信 | ⚡ 快 | ✅ | 即時預覽、減輕後端壓力 |
<?php
// 完全移除標籤,僅保留純文本
function plain_text(string $input): string {
return strip_tags($input);
}
// 允許極少數安全標籤(b, i, u, a)
function simple_html(string $input): string {
return strip_tags($input, '<b><i><u><a>');
}
// 輸出到 HTML 文本節點:必須轉義
function escape_html(string $input): string {
return htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// 組合:先過濾標籤,再轉義輸出
$userInput = '<script>alert(1)</script><b>粗體</b>';
echo escape_html(simple_html($userInput));
// 輸出:<script>alert(1)</script><b>粗體</b>
// 瀏覽器顯示:粗體(標籤被轉義為文本)
?>
<img src=x onerror=alert(1)> → strip_tags 保留 onerror 事件屬性<svg/onload=alert(1)>、Unicode 零寬字符干擾正則<b><script>...</b> 可能導致結構破壞<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:red"> 可劫持頁面<?php
// composer require ezyang/htmlpurifier
require_once 'vendor/autoload.php';
$config = HTMLPurifier_Config::createDefault();
// 核心配置
$config->set('HTML.Doctype', 'HTML 4.01 Transitional');
$config->set('HTML.Allowed', 'p,b,i,u,em,strong,a[href|title],ul,ol,li,br,img[src|alt|width|height],span[style],div[style]');
$config->set('CSS.AllowedProperties', 'font-weight,font-style,text-decoration,color,background-color,text-align,margin,padding');
$config->set('AutoFormat.RemoveEmpty', true);
$config->set('AutoFormat.AutoParagraph', true);
// 緩存配置(生產環境必開)
$config->set('Cache.SerializerPath', __DIR__ . '/storage/cache/htmlpurifier');
// URI 過濾:僅允許 http/https,禁止 javascript:、data:
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
$purifier = new HTMLPurifier($config);
// 使用
$dirty = '<script>alert(1)</script><p style="color:red">合法</p><a href="javascript:evil()">惡意</a><img src="x" onerror="alert(1)">';
$clean = $purifier->purify($dirty);
echo $clean;
// 輸出:合法
惡意
?>
| 業務場景 | 推薦方案 | 代碼片段 |
|---|---|---|
| 用戶名、標籤、搜索關鍵詞 | strip_tags + 長度限制 | substr(strip_tags($input), 0, 50) |
| 評論、留言(純文本) | htmlspecialchars 輸出時轉義 | 模板中 {$comment|escape} |
| 簡單公告、備註(允許粗體、連結) | strip_tags($input, '<b><i><a>') | 配合 rel="nofollow" 過濾 a 標籤 |
| 富文本編輯器內容(文章、帖子、商品詳情) | HTMLPurifier | 配置白名單、CSS 白名單、緩存 |
| API 輸出 JSON 供前端渲染 | 後端存原文/HTMLPurifier 淨化版,前端 DOMPurify 二次防護 | 雙重防護纏繞策略 |
<?php
class ContentSecurity {
private HTMLPurifier $purifier;
public function __construct() {
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,u,em,strong,a[href|title|rel],ul,ol,li,br,img[src|alt|width|height],blockquote,pre,code,h1,h2,h3,h3,h4,h5,h6');
$config->set('CSS.AllowedProperties', 'font-weight,font-style,text-decoration,color,background-color,text-align,margin,padding,border');
$config->set('Attr.EnableID', true);
$config->set('Cache.SerializerPath', sys_get_temp_dir() . '/htmlpurifier');
$this->purifier = new HTMLPurifier($config);
}
/** 入庫前淨化 */
public function purifyForStorage(string $html): string {
return $this->purifier->purify($html);
}
/** 輸出到 HTML 頁面(雙重保險) */
public function escapeForOutput(string $html): string {
// 若已信任存儲內容,可直接輸出
// 若不確定,再次淨化或轉義
return htmlspecialchars($html, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/** API 返回 JSON */
public function forApi(string $html): string {
return $html; // 前端負責 DOMPurify
}
}
// 使用
$security = new ContentSecurity();
$raw = $_POST['content'] ?? '';
$clean = $security->purifyForStorage($raw);
// 存入資料庫 $clean
// 前端渲染時:{!! $post->content !!} (Laravel Blade 信任輸出) 或 DOMPurify.sanitize()
?>
<!-- 頁面引入 -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.0/dist/purify.min.js"></script>
<script>
function renderUserContent(rawHtml, containerId) {
const clean = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['p','b','i','u','em','strong','a','ul','ol','li','br','img','span','div','blockquote','pre','code','h1','h2','h3','h4','h5','h6'],
ALLOWED_ATTR: ['href','title','rel','src','alt','width','height','style','class','id'],
ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
});
document.getElementById(containerId).innerHTML = clean;
}
</script>
Cache.SerializerPath 指向可寫目錄,首次解析後序列化,後續直接反序列化,速度提升 10-50 倍strip_tags
4. OpCache + JIT:PHP 8.1+ 開啟 JIT 可顯著提升 HTMLPurifier 解析速度
永遠不要信任用戶輸入。根據業務場景選型:
htmlspecialchars 輸出時轉義strip_tags 白名單 + 輸出轉義記住:strip_tags 不是安全函數,htmlspecialchars 是輸出層最後防線,HTMLPurifier 才是存儲層真正的淨化器。