HTML を regexp で解析するのは良くないことを知っています。また、すべてのケースで機能するとは限りません (スタック オーバーフローには、これに関するトピックがたくさんあります)。しかし、ホワイトリスト方式に基づいた正規表現で HTML をサニタイズしたかったのです。
以下に、私のコード (PHP 5.2 で記述) を示します。正常に動作しているように見えますが、セキュリティ上の問題があるかどうかはまだ疑問です。
それで、私は何か間違っていましたか?
基本的な原則は、Html_Sanitizer::sanitize() を使用することです
- この関数は、最初に属性のない許可されたタグをトークンに置き換えます。次に、属性を持つタグを解析し、それらもトークンに置き換えます。
- 次に、HTML タグが解析され、許可された属性が検出されます (cleanTag 関数を使用)。したがって、HTML タグは (できれば) 安全な方法で再構築されます。
- htmlspecialchars は、残りのコードがクリーンであることを確認するために使用されます
- トークンは安全なタグに置き換えられます。
コード:
class Html_Sanitizer
{
const VALIDATOR_CSS_UNIT = '(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0';
const VALIDATOR_URL = 'http://\\S+';
const VALIDATOR_CSS_PROPERTY = '[a-z\-]+';
const VALIDATOR_STYLE = '[^"]*';
protected static $_tags = 'a|b|blockquote|br|cite|d[ldt]|h[1-6]|i|img|li|ol|p|span|strong|u|ul';
protected static $_attributes = array(
'img' => array(
'width' => '[0-9]+',
'height' => '[0-9]+',
'src' => self::VALIDATOR_URL,
'style' => self::VALIDATOR_STYLE
),
'span' => array(
'style' => self::VALIDATOR_STYLE
),
'p' => array(
'style' => self::VALIDATOR_STYLE
),
'a' => array(
'href' => self::VALIDATOR_URL
)
);
protected static $_styleValidators = array(
'color' => '(\#[a-fA-F0-9]+)|([a-z ]+)',
'background-color' => '\#[a-zA-Z0-9]+',
'font-style' => '(normal|italic|oblique)',
'font-size' => '[\-a-z]+',
'margin-left' => self::VALIDATOR_CSS_UNIT,
'margin-right' => self::VALIDATOR_CSS_UNIT,
'text-align' => '(left|right|center|justify)',
'text-indent' => self::VALIDATOR_CSS_UNIT,
'text-decoration' => '(none|overline|underline|blink|line-through)',
'width' => self::VALIDATOR_CSS_UNIT,
'height' => self::VALIDATOR_CSS_UNIT
);
public static function sanitize($str)
{
$tokens = array();
//tokenize opening tags with no attributes
$pattern = '#<(/)?('. self::$_tags .')>#';
$replace = '__SAFE_TAG_$1$2__';
$str = preg_replace($pattern, $replace, $str);
// tokenize tags with attributes
$pattern = '#<('. self::$_tags .')(?:\s+(?:[a-z]+)="(?:[^"\\\]*(?:\\\"[^"\\\]*)*)")*\s*(/)?>#';
preg_match_all($pattern, $str, $matches, PREG_SET_ORDER);
foreach($matches as $i => $match) {
$tokens[$i] = self::cleanTag($match[1], $match[0]);
$str = str_replace($match[0], '__SAFE_TOKEN_'.$i.'__', $str);
}
$str = htmlspecialchars($str);
foreach ($tokens as $i => $cleanTag) {
$str = str_replace('__SAFE_TOKEN_'.$i.'__', $cleanTag, $str);
}
$pattern = '#__SAFE_TAG_(/?(?:'. self::$_tags .'))__#';
$replace = '<$1>';
$str = preg_replace($pattern, $replace, $str);
return $str;
}
public static function cleanTag($tag, $str)
{
$cleanTag = '<' . $tag;
if ($tag === 'a') {
$cleanTag .= ' rel="nofolow" target="_blank"';
}
if (isset(self::$_attributes[$tag])) {
foreach(self::$_attributes[$tag] as $attr => $attrPattern) {
$pattern = '#'.$attr.'="('. $attrPattern .')"#';
preg_match($pattern, $str, $match);
if (isset($match[1])) {
if ($attr == 'style') {
$cleanTag .= ' style="' . self::cleanStyle($match[1]) . '"';
} else {
$cleanTag .= ' ' . $attr . '="' . $match[1] . '"';
}
}
}
}
if ($tag === 'img') {
$cleanTag .= ' /';
}
$cleanTag .= '>';
return $cleanTag;
}
public static function cleanStyle($style)
{
$cleanStyle = '';
foreach(self::$_styleValidators as $stl => $stlPattern) {
$pattern = '#[; ]?' . $stl . '\s*:\s*(' . $stlPattern . ')\s*;#i';
preg_match($pattern, $style, $match);
if (isset($match[1])) {
$cleanStyle .= ($cleanStyle ? ' ' : '') . $stl . ':' . $match[1] . ';';
}
}
return $cleanStyle;
}
}