使用者輸入富文本 (如編輯器內容),需**只保留**特定標籤 (如 ``, ` `, ` 白名單過濾是富文本安全的核心。**請使用 DOMDocument 或 HTML Purifier 等成熟解析器**,勿自寫正則。定義清晰的標籤/屬性白名單,強制安全屬性 (`rel=noopener`),配合 CSP 標頭,構建完整防禦體系。`) 及其**特定屬性** (如 `href`, `target`, `src`, `alt`),移除所有其他標籤、屬性、Script、事件處理器。
核心邏輯 (基於原文函式重構)
現代化實作 (PHP 8.0+、DOMDocument 方案,更健壯)
<?php
function filterHtmlAllowlist(string $html, array $allowlist): string {
// 允許清單格式: ['a' => ['href','target','rel'], 'img' => ['src','alt','width','height'], 'p' => ['class']]
$dom = new DOMDocument();
// 載入片段,避免自動補全 html/body
$dom->loadHTML('<meta charset="utf-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new DOMXPath($dom);
// 遍歷所有元素
foreach ($xpath->query('//*') as $node) {
$tag = strtolower($node->nodeName);
if (!isset($allowlist[$tag])) {
// 標籤不在白名單:移除節點,但保留子節點 (避免內容消失)
$parent = $node->parentNode;
while ($node->childNodes->length > 0) {
$parent->insertBefore($node->childNodes->item(0), $node);
}
$parent->removeChild($node);
continue;
}
// 標籤在白名單:過濾屬性
$allowedAttrs = $allowlist[$tag];
foreach ($node->attributes as $attr) {
if (!in_array($attr->nodeName, $allowedAttrs, true)) {
$node->removeAttribute($attr->nodeName);
}
}
// 強制安全屬性
if ($tag === 'a' && !$node->hasAttribute('rel')) {
$node->setAttribute('rel', 'noopener noreferrer');
}
}
// 序列化 body 內容
$body = $dom->getElementsByTagName('body')->item(0);
if (!$body) return ''; // 理論上不會發生
$result = '';
foreach ($body->childNodes as $child) {
$result .= $dom->saveHTML($child);
}
return $result;
}
// 使用範例
$allowlist = [
'a' => ['href', 'target', 'rel', 'title'],
'img' => ['src', 'alt', 'width', 'height', 'loading'],
'p' => ['class', 'style'],
'br' => [],
'strong' => [], 'em' => [], 'u' => [], 'span' => ['style'],
'ul' => [], 'ol' => [], 'li' => [],
];
$dirty = '<script>alert(1)</script><a href="https://evil.com" onclick="steal()" target="_blank">Link</a><img src="x" onerror="alert(1)">';
$clean = filterHtmlAllowlist($dirty, $allowlist);
// 結果: Link
?>
為什麼不用正則?
效能優化
總結