在處理使用者輸入的 HTML 內容時,為了防止 XSS 攻擊或排版混亂,我們通常需要移除不安全或不需要的屬性(如 onclick、style、onload 等)。本文介紹一個 HtmlAttributeFilter 類別,它能依據「允許清單」、「標籤例外規則」與「忽略標籤」三種策略,精準過濾 HTML 標籤屬性,保留合法內容。
strip($str):主入口,回傳過濾後的 HTML 字串。setAllow(array $attrs):全域允許保留的屬性(如 id、class、title)。setException(array $rules):針對特定標籤允許額外屬性(如 a 標籤保留 href、ul 保留 class)。setIgnore(array $tags):完全跳過過濾的標籤(如 img 保留所有屬性)。preg_replace 替換原標籤。<?php
class HtmlAttributeFilter {
private $_str = ''; private $_allow = array(); private $_exception = array(); private $_ignore = array();
public function strip($str) {
$this->_str = $str;
if (is_string($this->_str) && strlen($this->_str) > 0) {
$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($param = array()) { $this->_allow = $param; }
public function setException($param = array()) { $this->_exception = $param; }
public function setIgnore($param = array()) { $this->_ignore = $param; }
private function findElements() {
$nodes = array();
preg_match_all("/<([^ !\\/>\\n]+)([^>]*)>/i", $this->_str, $elements);
foreach ($elements[1] as $k => $tag) {
if ($elements[2][$k]) {
if (!in_array($tag, $this->_ignore)) {
$nodes[] = array('literal'=>$elements[0][$k], 'name'=>$tag, 'attributes'=>$elements[2][$k]);
}
}
}
return empty($nodes) ? $this->_str : $nodes;
}
private function findAttributes($nodes) {
foreach ($nodes as &$node) {
preg_match_all("/([^ =]+)\\s*=\\s*[\"|']{0,1}([^\"']*)[\"|']{0,1}/i", $node['attributes'], $atts);
$node['attributes'] = array();
if ($atts[1]) {
foreach ($atts[1] as $ak => $an) {
$node['attributes'][] = array('literal'=>$atts[0][$ak], 'name'=>$an, 'value'=>$atts[2][$ak]);
}
}
}
return $nodes;
}
private function removeAttributes($nodes) {
foreach ($nodes as $node) {
$newAttrs = '';
if (is_array($node['attributes'])) {
foreach ($node['attributes'] as $att) {
if (in_array($att['name'], $this->_allow) || $this->isException($node['name'], $att['name'])) {
$newAttrs = $this->createAttributes($newAttrs, $att['name'], $att['value']);
}
}
}
$replacement = $newAttrs ? "<{$node['name']} $newAttrs>" : "<{$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 = array('^'=>'\\^','['=>'\\[','.'=>'\\.','$'=>'\\$','{'=>'\\{','*'=>'\\*','('=>'\\(','\\'=>'\\\\','/'=>'\\/','+'=>'\\+',')'=>'\\)','|'=>'\\|','?'=>'\\?','<'=>'\\<','>'=>'\\>');
return strtr($str, $map);
}
}
?>
<?php
require('HtmlAttributeFilter.class.php');
$html = '<div class="bd" id="main" onclick="alert(1)"><a href="https://example.com" title="link">連結</a><img src="pic.jpg" width="100"></div>';
$filter = new HtmlAttributeFilter();
$filter->setAllow(array('id', 'class')); // 全域允許 id, class
$filter->setException(array('a' => array('href', 'title'))); // a 標籤額外允許 href, title
$filter->setIgnore(array('img')); // img 完全不處理
echo '原始: ' . htmlspecialchars($html) . '<br><br>';
echo '過濾後: ' . htmlspecialchars($filter->strip($html));
?>
原始: <div class="bd" id="main" onclick="alert(1)"><a href="https://example.com" title="link">連結</a><img src="pic.jpg" width="100"></div> 過濾後: <div class="bd" id="main"><a href="https://example.com" title="link">連結</a><img src="pic.jpg" width="100"></div>
div 保留 class、id,移除 onclick;a 保留 href、title;img 完全保持原樣。此類別靈活且易擴充,適合 CMS、論壇、留言板等需嚴格控制 HTML 輸出的場景。