缘由

久没上我的测试站点,发现多了不少评论(明明我从来没公开过链接,却还是被扫到了 :黑线: ),其中一条引起我的注意: ***<script>alart(*****);</script>;*****<script>window.location.href='http://*****';</script>******,打开评论所在页面一看,果然是 xss攻击 。测试站点没有装Wordfence,所以才让攻击者有可承之机,但我平时不会开放测试站的访客访问,多装一个Wordfence很浪费资源,于是我决定修改下评论功能。小站评论框上方一直有这样一句话 Markdown Supported while </> Forbidden,但实际上评论html也是可以解析的,我觉得从这入手比较好,正巧Sakura主题评论插入图片的方式使用的是安全的BBCode,如果再加入仅允许Markdown评论的功能,那绝大多数XSS就直接被干掉了 :酷2: 。话不多说,下面记录下我的修改过程。

评论使用Ajax

此条为必须,采用Ajax提交评论可以在评论内容写入数据库之前再对评论进行一次检查。Sakura已自带Ajax评论,所以此步省略,Ajax评论引入也很简单,如需了解更多,参考这位大佬写的 WordPress Ajax 提交评论的实现,简单易懂。

过滤无效邮箱

WordPress自带了一个检查评论者邮箱是否为正常邮箱的功能,所以我不再重复添加了。这里主要是验证邮箱是否有效,用到的是 checkdnsrr() 函数,查询评论者邮箱的所属域名有没有MX记录,使用方法如下:
以Sakura主题为例,检查Ajax传入的评论参数 $incoming_comment ,得到评论邮箱 $incoming_comment['comment_author_email'] ,使用 explode()array_pop() 函数得到邮箱域名,再用 checkdnsrr() 函数函数检查域名DNS解析中有无 MX 记录。代码如下:

function spirit_comment_check($incoming_comment) {
    $isSpam = 0;
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
        $isSpam = 1;
    }
    if(!$isSpam)
        return $incoming_comment;
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

禁止html代码

我使用的WP-Editor.md插件,支持评论Markdown,所以无需再引入其他文件来解析评论中的Markdown,那么重点就在如何禁止评论使用HTML标签。PHP自带了一个 strip_tags() 函数,可以把字符串中的HTML标签全部过滤掉,于是就有了下面的代码。

function spirit_comment_check($incoming_comment) {
    $isSpam = 0;
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
        $isSpam = 1;
    }else{
        if($incoming_comment['comment_content'] != strip_tags($incoming_comment['comment_content'])){
            siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
            $isSpam = 1;
        }
    }
    if(!$isSpam)
        return $incoming_comment;
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

但这又有个问题,如果评论者输入的代码块中,包含了 < 的HTML标签,那么就不能提交,于是我又想到一个办法,去掉评论内容中的代码块之后再检查有无HTML标签,下面是修改版:

function spirit_comment_check($incoming_comment) {
    $isSpam = 0;
    $re = '/```([\s\S]*?)```[\s]*|`{1,2}[^`](.*?)`{1,2}|\[.*?\]\([\s\S]*?\)/m';
    if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
        siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
        $isSpam = 1;
    }else{
        if(preg_replace($re,'temp',$incoming_comment['comment_content']) != strip_tags(preg_replace($re,'temp',$incoming_comment['comment_content']))){
            siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
            $isSpam = 1;
        }
    }
    if(!$isSpam)
        return $incoming_comment;
}
if(!is_user_logged_in())
    add_filter( 'preprocess_comment', 'spirit_comment_check' );

正则解释如下:
```([\s\S]*?)```[\s]*过滤掉代码片段,`{1,2}[^`](.*?)`{1,2}过滤掉行内代码,\[.*?\]\([\s\S]*?\)过滤掉链接,因为有时候链接标题也会带 < 字符。开始我也担心过滤掉链接会增加被XSS攻击的风险(类似于 [Click Me](javascript:alert(***)) 这样的语句),但发现WordPress的kses会自动转义这样的语句,所以就放心使用啦~

打开评论HTML标签限制

完成之后我使用访客模式打开了小站,测试评论功能时发现一个问题,评论部分Markdown格式不能转换,比如:

/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));

正常情况下会解析为

<pre><code class="language-javascript ">
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));
</code></pre>

但WP-Editor.md将其解析为了

<code>
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
  var a = 1, b = 1;
  var tmp;
  while (--n >= 0) {
    tmp = a;
    a += b;
    b = tmp;
  }
  return a;
}
document.write(fib(10));
</code>

这样的话代码高亮就失效了,体验很是不好。此外还有标题、表格、列表等也不会解析...我反复检查了插件,又翻了不少WordPress的Hook,把插件改了又改,始终没有修复这个Bug(主要还是我太菜了 :捂脸: ,最终只能从修改WordPress限制入手了,将下面代码加到functions.php

//打开评论HTML标签限制
function allow_more_tag_in_comment() {
    global $allowedtags;
    $allowedtags['pre'] = array('class'=>array());
    $allowedtags['code'] = array('class'=>array());
    $allowedtags['h1'] = array('class'=>array());
    $allowedtags['h2'] = array('class'=>array());
    $allowedtags['h3'] = array('class'=>array());
    $allowedtags['h4'] = array('class'=>array());
    $allowedtags['h5'] = array('class'=>array());
    $allowedtags['ul'] = array('class'=>array());
    $allowedtags['ol'] = array('class'=>array());
    $allowedtags['li'] = array('class'=>array());
    $allowedtags['td'] = array('class'=>array());
    $allowedtags['th'] = array('class'=>array());
    $allowedtags['tr'] = array('class'=>array());
    $allowedtags['table'] = array('class'=>array());
    $allowedtags['thead'] = array('class'=>array());
    $allowedtags['tbody'] = array('class'=>array());
    $allowedtags['span'] = array('class'=>array());
}
add_action('pre_comment_on_post', 'allow_more_tag_in_comment');

我添加了部分常用的标签,如果后续遇到不能解析的可尝试在里面继续加入更多标签。最后关闭Wordfence的xss防护,防止不能提交带有 <script 的代码块。
以上就是本次的修改历程,你有什么看法或者是对文中功能的优化吗?欢迎在评论区与我探讨。 :喝茶: