使用者輸入 HTML 內容時,需過濾危險屬性(onclick、style 等)防止 XSS。HtmlAttributeFilter 提供白名單機制、標籤級例外、忽略標籤等靈活設定。
id、class、title)a 標籤允許 href)img 保留所有屬性)<?php
/** HTML 屬性過濾器
* Date: 2013-09-22
* Author: fdipzone
* Ver: 1.0
*/
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 $key => $tag) {
if ($elements[2][$key]) {
$literal = $elements[0][$key];
$name = $elements[1][$key];
$attrs = $elements[2][$key];
if (is_array($this->_ignore) && !in_array($name, $this->_ignore)) {
$nodes[] = array('literal'=>$literal, 'name'=>$name, 'attributes'=>$attrs);
}
}
}
return $nodes ? $nodes : $this->_str;
}
/** 解析屬性 */
private function findAttributes($nodes) {
foreach ($nodes as &$node) {
preg_match_all("/([^ =]+)\s*=\s*[\"|']{0,1}([^\"']*)[\"|']{0,1}/i", $node['attributes'], $attrs);
$atts = array();
if ($attrs[1]) {
foreach ($attrs[1] as $k => $aname) {
$atts[] = array(
'literal' => $attrs[0][$k],
'name' => $attrs[1][$k],
'value' => $attrs[2][$k]
);
}
}
$node['attributes'] = $atts;
}
return $nodes;
}
/** 移除不允許屬性並重建標籤 */
private function removeAttributes($nodes) {
foreach ($nodes as $node) {
$new_attrs = '';
if (is_array($node['attributes'])) {
foreach ($node['attributes'] as $attr) {
$allowed = in_array($attr['name'], $this->_allow);
$excepted = $this->isException($node['name'], $attr['name']);
if ($allowed || $excepted) {
$new_attrs = $this->createAttributes($new_attrs, $attr['name'], $attr['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) {
$conv = array(
'^'=>'\^', '['=>'\[', '.'=>'\.', '$'=>'\$', '{'=>'\{', '*'=>'\*',
'('=>'\(', '\\'=>'\\\\', '/'=>'\/', '+'=>'\+', ')'=>'\)', '|'=>'\|',
'?'=>'\?', '<'=>'\<', '>'=>'\>'
);
return strtr($str, $conv);
}
}
?>
<?php
require('HtmlAttributeFilter.class.php');
$str = '<div class="bd" id="main"><ul class="list"><li><img src="pic.jpg" width="100"><a href="https://example.com" onclick="alert(1)">連結</a></li></ul></div>';
$obj = new HtmlAttributeFilter();
// 全域允許 id
$obj->setAllow(array('id'));
// 標籤例外:a 允許 href,ul 允許 class
$obj->setException(array(
'a' => array('href'),
'ul' => array('class')
));
// 忽略 img 標籤(保留所有屬性)
$obj->setIgnore(array('img'));
echo '原始:<br>';
echo htmlspecialchars($str) . '<br><br>';
echo '過濾後:<br>';
echo htmlspecialchars($obj->strip($str));
?>
原始: <div class="bd" id="main"><ul class="list"><li><img src="pic.jpg" width="100"><a href="https://example.com" onclick="alert(1)">連結</a></li></ul></div> 過濾後: <div id="main"><ul class="list"><li><img src="pic.jpg" width="100"><a href="https://example.com">連結</a></li></ul></div>
div:移除 class,保留 id(全域白名單)ul:保留 class(標籤例外)img:完全保留(忽略標籤)a:保留 href(標籤例外),移除 onclick(危險屬性)HTMLPurifier 做完整 XSS 防護htmlspecialchars 或 CSP 標頭expression()、url(javascript:) 等樣式型攻擊新專案強烈建議使用 HTMLPurifier:
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'div[id],ul[class],li,img[src|width|height],a[href]');
$purifier = new HTMLPurifier($config);
$clean = $purifier->purify($dirty_html);