PHP 是一门神奇的语言,神奇在于你可能不清楚大多数函数的实现,但只要调用得当,就能达到你想要的效果。

Destoon 框架:跟主流框架不太相同,Destoon 本身实现了一个包含电商的网站,包括 pc 端和移动端。刚接触这个框架的时候,感觉路由混乱,到处都是入口。这应该是之前习惯了单一入口框架的结果。熟悉之后发现,Destoon 很棒,逻辑结构清晰、层级分明。所以这就是一个熟悉的过程。另外让人惊叹地就是 Destoon 自带的一些函数库文件了。

今天主要解析 include\safe_func.php 里涉及转义 html 特殊字符(dhtmlspecialchars)和防止 js 脚本注入(dsafe)的函数。这两个函数主要用于富文本之类的内容转义和替换。

富文本内容存储

富文本内容在存储进入数据库之前需要对一些 html 特殊字符做处理,并且要能够防止 js 脚本注入。在进入正文之前需要对几个概念做一下定义解释。

转义字符

转义字符是很多程序语言、数据格式和通信协议的形式文法的一部分。对于一个给定的字母表,一个转义字符的目的是开始一个字符序列,使得转义字符开头的该字符序列具有不同于该字符序列单独出现时的语义。因此转义字符开头的字符序列被叫做转义序列。

实体字符

在 HTML 中,某些字符是预留的。在 HTML 中不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签。如果希望正确地显示预留字符,我们必须在 HTML 源代码中使用字符实体(character entities)。

如需显示小于号,我们必须这样写:&lt;&#60;。这其实就是 html 的转义字符,xml 等标记语言转义字符由 & 加上字符或数字,末尾加上分号。其他脚本语言,如 php,js,java 都是反斜杠 \ 标记转义字符。

当富文本内容为不可信来源时,就需要对其中的一些内容做处理。

function dsafe($string, $type = 1) {
    if(is_array($string)) {
        return array_map('dsafe', $string);
    } else {
        if($type) {
            $string = str_replace('<em></em>', '', $string);
            $string = preg_replace("/\<\!\-\-([\s\S]*?)\-\-\>/", "", $string);
            $string = preg_replace("/\/\*([\s\S]*?)\*\//", "", $string);
            $string = preg_replace("/&#([a-z0-9]{1,})/i", "<em></em>&#\\1", $string);
            $match = array("/s[\s]*c[\s]*r[\s]*i[\s]*p[\s]*t/i","/d[\s]*a[\s]*t[\s]*a[\s]*\:/i","/b[\s]*a[\s]*s[\s]*e/i","/e[\\\]*x[\\\]*p[\\\]*r[\\\]*e[\\\]*s[\\\]*s[\\\]*i[\\\]*o[\\\]*n/i","/i[\\\]*m[\\\]*p[\\\]*o[\\\]*r[\\\]*t/i","/on([a-z]{2,})([\(|\=|\s]+)/i","/about/i","/frame/i","/link/i","/meta/i","/textarea/i","/eval/i","/alert/i","/confirm/i","/prompt/i","/cookie/i","/document/i","/newline/i","/colon/i","/<style/i","/\\\x/i");
            $replace = array("s<em></em>cript","da<em></em>ta:","ba<em></em>se","ex<em></em>pression","im<em></em>port","o<em></em>n\\1\\2","a<em></em>bout","f<em></em>rame","l<em></em>ink","me<em></em>ta","text<em></em>area","e<em></em>val","a<em></em>lert","/con<em></em>firm/i","prom<em></em>pt","coo<em></em>kie","docu<em></em>ment","new<em></em>line","co<em></em>lon","<sty1e","\<em></em>x");
            return str_replace(array('isShowa<em></em>bout', 'co<em></em>ntrols'), array('isShowAbout', 'controls'), preg_replace($match, $replace, $string));
        } else {
            return str_replace(array('<em></em>', '<sty1e'), array('', '<style'), $string);
        }
    }
}

array_map 实现字符串数组的替换处理,主要替换方法就是 str_replacepreg_replace。前者直接匹配字符串并替换,后者是通过正则表达式匹配内容并替换。

当 type 等于 1(默认)时,可以从最后的一组替换看出,是要将一些设计到 js 脚本内容部分给破坏掉,这样就可以使得即使注入 js 脚本也不会执行。

function dhtmlspecialchars($string) {
    if(is_array($string)) {
        return array_map('dhtmlspecialchars', $string);
    } else {
        $string = htmlspecialchars($string, ENT_QUOTES, DT_CHARSET == 'GBK' ? 'GB2312' : 'UTF-8');
        return str_replace('&amp;', '&', $string);
    }
}

dhtmlspecialchars 基于 php 库函数 htmlspecialchars()。我以为会在录入数据库的时候将富文本内容中的 html 特殊字符转化为字符实体,但 Destoon 并没有这么做。其实只要富文本内容可靠,并且没有携带其他样式参数或者 js 脚本内容,直接输出是没有问题的。但这样的理想化场景不多见。

htmlspecialchars 主要针对 & " ' < > 几个特殊字符做处理,测试示例如下:

php > echo htmlspecialchars('<a href="https://www.seasidecrab.com/?s=1&a=2&name=\"jason">relocate</a>');

&lt;a href=&quot;https://www.seasidecrab.com/?s=1&amp;a=2&amp;name=\&quot;jason&quot;&gt;relocate&lt;/a&gt;

通过 htmlspecialchars() 函数处理过后的富文本,输出内容为纯文本,HTML 实体字符 会被浏览器识别并展示,但仅仅展示而已,不在 DOM 文档中担当节点角色。与之类似的叫 htmlentities()。这个函数是可以转换所有具有 HTML 实体的字符。

富文本内容输出

Destoon 对富文本输出内容控制及其严格。

$conten = nl2br(dsubstr(trim(strip_tags($content)), $length, '...'));

strip_tags() 函数作用是从字符串中去除 HTML 和 PHP 标记。可以强制获取到文本内容部分,过滤掉所有标签。它还可以过滤空字节(官网上翻译成空字符,会让人误以为和函数 trim() 功能覆盖了呢,其实不是,它过滤的是空字节,如 \x00)。测试示例如下:

php > echo strip_tags('<a href="https://www.seasidecrab.com/?s=1&a=2&name=jason"
>relocate</a>');
relocate
php > echo strip_tags('\x00');
\x00
php > echo strip_tags("\x00");

nl2br() 是在换行符 \r\n 或者 \n 前面加一个 <br/>(其实是替换),这样换行就能被 html 显示出来。

function dsubstr($string, $length, $suffix = '', $start = 0) {
    if($start) {
        $tmp = dsubstr($string, $start);
        $string = substr($string, strlen($tmp));
    }
    $strlen = strlen($string);
    if($strlen <= $length) return $string;
    $string = str_replace(array('&quot;', '&lt;', '&gt;'), array('"', '<', '>'), $string);
    $length = $length - strlen($suffix);
    $str = '';
    if(DT_CHARSET == 'UTF-8') {
        $n = $tn = $noc = 0;
        while($n < $strlen)    {
            $t = ord($string{$n});
            if($t == 9 || $t == 10 || (32 <= $t && $t <= 126)) {
                $tn = 1; $n++; $noc++;
            } elseif(194 <= $t && $t <= 223) {
                $tn = 2; $n += 2; $noc += 2;
            } elseif(224 <= $t && $t <= 239) {
                $tn = 3; $n += 3; $noc += 2;
            } elseif(240 <= $t && $t <= 247) {
                $tn = 4; $n += 4; $noc += 2;
            } elseif(248 <= $t && $t <= 251) {
                $tn = 5; $n += 5; $noc += 2;
            } elseif($t == 252 || $t == 253) {
                $tn = 6; $n += 6; $noc += 2;
            } else {
                $n++;
            }
            if($noc >= $length) break;
        }
        if($noc > $length) $n -= $tn;
        $str = substr($string, 0, $n);
    } else {
        for($i = 0; $i < $length; $i++) {
            $str .= ord($string{$i}) > 127 ? $string{$i}.$string{++$i} : $string{$i};
        }
    }
    $str = str_replace(array('"', '<', '>'), array('&quot;', '&lt;', '&gt;'), $str);
    return $str == $string ? $str : $str.$suffix;
}

dsubstr() 除了限定字符长度外,还对处理对象字符串做转义符替换,这就相当于执行了 htmlspecialchars()。使得最终输出的内容在 html 中变成了字符串,后续通过 nl2br() 想将段落标注显示出来,但已经对富文本做了破坏,没有办法像在富文本编辑器里显示的那样了。

没办法,要便捷就要牺牲安全性,要安全就要牺牲便捷性,或者做大量地适配修改。