處理用戶提交的 HTML 內容(如富文本編輯器輸出)時,需移除不安全屬性(onclick、style、onload 等)同時保留必要屬性(href、src、class、id)。本文實作 HtmlAttributeFilter 類別,支援全域白名單、標籤級例外、忽略標籤三層規則,精準控制屬性保留與移除。
setAllow(['id', 'class', 'title']),所有標籤通用保留屬性。setException(['a' => ['href', 'target'], 'img' => ['src', 'alt']]),特定標籤額外允許屬性。setIgnore(['script', 'style']),完全跳過處理(保留原樣或由其他機制處理)。<?php
class HtmlAttributeFilter {
private $_str = '';
private $_allow = []; // 全域白名單
private $_exception = []; // 標籤例外
private $_ignore = []; // 忽略標籤
public function strip($str) {
$this->_str = $str;
if (!is_string($this->_str) || strlen($this->_str) === 0) return $this->_str;
$this->_str = strtolower($this->_str); // 統一小寫處理
$res = $this->findElements();
if (is_string($res)) return $res; // 無符合元素直接返回
$nodes = $this->findAttributes($res);
$this->removeAttributes($nodes);
return $this->_str;
}
public function setAllow(array $param) { $this->_allow = $param; }
public function setException(array $param) { $this->_exception = $param; }
public function setIgnore(array $param) { $this->_ignore = $param; }
private function findElements() {
$nodes = [];
// 比對 結構
preg_match_all('/<([^ !\/>\\n]+)([^>]*)>/i', $this->_str, $elements);
foreach ($elements[1] as $idx => $tag) {
$attrs = $elements[2][$idx];
if ($attrs && (!is_array($this->_ignore) || !in_array($tag, $this->_ignore))) {
$nodes[] = [
'literal' => $elements[0][$idx],
'name' => $tag,
'attributes' => $attrs
];
}
}
return $nodes ? $nodes : $this->_str;
}
private function findAttributes($nodes) {
foreach ($nodes as &$node) {
// 比對 attr="value" 或 attr='value' 或 attr=value
preg_match_all('/([^ =]+)\s*=\s*["\']{0,1}([^"\']*)["\']{0,1}/i', $node['attributes'], $atts);
$node['attributes'] = [];
if ($atts[1]) {
foreach ($atts[1] as $i => $name) {
$node['attributes'][] = [
'literal' => $atts[0][$i],
'name' => $name,
'value' => $atts[2][$i]
];
}
}
}
return $nodes;
}
private function removeAttributes($nodes) {
foreach ($nodes as $node) {
$new_attrs = '';
if (is_array($node['attributes'])) {
foreach ($node['attributes'] as $att) {
$keep = (is_array($this->_allow) && in_array($att['name'], $this->_allow)) ||
$this->isException($node['name'], $att['name']);
if ($keep) {
$new_attrs = $this->createAttributes($new_attrs, $att['name'], $att['value']);
}
}
}
$replacement = $new_attrs ? "<{$node['name']} $new_attrs>" : "<{$node['name']}>";
$this->_str = preg_replace('/' . $this->protect($node['literal']) . '/', $replacement, $this->_str);
}
}
private function isException($tag, $attr) {
return isset($this->_exception[$tag]) && in_array($attr, $this->_exception[$tag]);
}
private function createAttributes($str, $name, $value) {
return $str . ($str ? ' ' : '') . "$name=\"$value\"";
}
private function protect($str) {
$map = ['\\' => '\\\\', '^' => '\\^', '[' => '\\[', '.' => '\\.', '$' => '\\$',
'{' => '\\{', '*' => '\\*', '(' => '\\(', ')' => '\\)', '|' => '\\|',
'+' => '\\+', '?' => '\\?', '<' => '\\<', '>' => '\\>'];
return strtr($str, $map);
}
}
?>
<?php
require 'HtmlAttributeFilter.class.php';
$html = '';
$filter = new HtmlAttributeFilter();
$filter->setAllow(['id', 'class']); // 全域保留 id, class
$filter->setException([
'a' => ['href', 'target'], // 額外允許 href, target
'img' => ['src', 'alt', 'width', 'height'] //
額外允許 src, alt, width, height
]);
$filter->setIgnore(['script', 'style']); // 忽略 script/style 標籤
echo '原始:' . htmlspecialchars($html) . '
';
echo '過濾後:' . htmlspecialchars($filter->strip($html));
原始:
過濾後:
ignore 列表 → 完全跳過,保留原樣。allow 白名單 → 保留。exception 例外列表 → 保留。