打击垃圾信息,构建更健康的网站

作者:Eli White

在包含用户生成的内容的网站上存在垃圾信息,这是不争的事实,但您可以采取一些措施将其风险和影响降至最低。

2011 年 4 月发布

与运行网站(其上允许用户发布评论或个人资料)的任何人交流时,很快就会讨论到垃圾信息。垃圾信息已成为网站上的一大祸患,包括我个人工作过的网站,例如 Digg.com。

在本文中,我将介绍多种方法来阻止垃圾信息影响您自己的 Web 属性。

什么是垃圾信息,为什么要阻止垃圾信息

首先,我们来定义究竟什么是垃圾信息。在本文中,我们将垃圾信息定义为网站上用户生成的脱离上下文的所有内容(如评论、帖子或文章)。

那么,人们为什么发布垃圾信息呢?最明显的原因是试图推销某些东西和赚钱。通常,垃圾信息将推销某人试图销售的产品或服务。更常见的是,垃圾信息将包括指向外部网站的链接,发布者希望借此诱惑毫无戒心的人们,从而增加广告收入。或者,垃圾信息发送者希望直接影响搜索引擎排名,因为您的合法域中包含更多指向他们站点的链接,他们就会获得搜索器信誉。

当我们讨论这些用于处理垃圾信息的方法时,请不要立即实施每种选择。这将破坏合法用户的用户体验,并会在可能对您自己的状况没有任何作用的解决方案上浪费大量时间。相反,应确保首先研究您所获取的垃圾信息的种类。分析这些垃圾信息。看看哪种解决方案可能最适合您的情况,产生最低的误报(停止有效帖子)可能性,并对合法用户的用户体验影响最小。

制造障碍

一个有效的目标是只需增加垃圾信息发送者发布信息的难度。对垃圾信息发送者而言,发布信息的难度越大,他们就越可能离开您的网站去别的地方。

适度控制
第一种方法实际上是最有效的方法:实施适度控制,用户生成的内容仅在某人审查并批准后才显示。

但是这样做成本很高,因此很少采用。它非常适合小型个人博客,您自己就可以处理几十条评论。但是,它无法扩展。如果您所运行的网站每天收到来自用户的数百万条内容,如何经济高效地浏览所有这些内容?因此,您需要寻求自动方法。

需要登录
如果您禁止任何人匿名发布评论,并要求创建用户帐户才能获得权限,那么您实际上做了两件事。首先,您制造了一个基本障碍:仅凭简单的脚本已无法直接向您的网站发布内容。现在垃圾信息发送者必须创建帐户,告诉他们的脚本如何登录到您的网站,来回传递 cookie,并且其他方面的行为也像真实用户一样。这将阻止最简单的垃圾信息发送者,我们相信许多垃圾信息发送者会转而尝试其他目标。

您在将来还会受益颇丰。我们稍后将讨论的许多方法都依赖于能够跟踪个人的操作。通过需要登录,可以非常容易地保存个人的统计信息并跟踪他们在您网站上的操作。这可以帮助您确定这个人是合法用户还是垃圾信息发送者。

让用户证明他们是人

下一类方法的核心是证明当前是某个人(而不是计算机脚本)在执行任务。这是打击垃圾信息中的重要一步,因为它将阻止所有自动垃圾信息攻击您的系统。当然,这仍然只是局部解决方案。虽然它可以阻止临时垃圾信息发送者,但“人类垃圾信息”呈增长趋势,甚至会雇用人员坐下来手动在网站上输入垃圾帖子。由于这些垃圾信息发送者是真实的人,因此对于本部分中的任何方法来说,他们看起来都是合法的。

CSRF 保护

跨站点请求伪造 (CSRF) 是常见的网站漏洞,是要实施保护的重要安全问题。这与垃圾信息没有直接关系,但由于该漏洞的性质,您所实施的保护碰巧可以阻止更多的自动垃圾信息。

CSRF 的传统解决方案需要您针对每个用户在 PHP 会话中存储唯一的 ID。然后,当向该用户显示提交表单时,您将唯一的 ID 作为隐藏的表单域。提交表单时,服务器将检查会话的唯一 ID 副本是否与随表单提交的 ID 匹配。这样,您需要用户实际加载表单以检索正确的隐藏域值。

因此,除了阻止严重的安全漏洞之外,这还意味着使用您站点的自动脚本需要执行相同的操作。它需要加载网页,分析网页的所有表单元素,然后重新提交表单元素以及相应的 cookie 以跟踪会话。这当然可以实现,但简单脚本会严重受阻。

CAPTCHA

CAPTCHA(全自动区分计算机和人类的图灵测试)是基本图灵测试的一种尝试。最常见的 CAPTCHA 形式是在图像中显示人类可以轻松阅读而计算机无法读懂的一些字母或单词。由于计算机可以更好地执行光学字符识别 (OCR),并且可以自己将单词图像处理为单词,因此 CAPTCHA 技术变得更强效。现在,CAPTCHA 可以包含高度扭曲的字母,使人们更难以辨别它们,但希望脚本无法识别它们。其中 CAPTCHA 的一些示例如下所示:

combating-spam-f1 combating-spam-f2

 

第二个示例很有用,因为它由一种名为 reCAPTCHA 的免费服务提供。您可以轻松地使用 reCAPTCHA,让 reCAPTCHA 去考虑如何使用更好的 OCR 软件推出新的解决方案来打击垃圾信息发送者。与此同时,reCAPTCHA 实际上还帮助辨认旧书,因此它是一种非常有用的服务。

将 PHP 与 reCAPTCHA 集成很容易。首先,在其网站上注册一个免费帐户。它将为您提供公钥和私钥:

combating-spam-f3


然后,您下载它们提供的 PHP 库,进行简单的函数调用以生成 CAPTCHA,例如:

<form method="POST" action="/submission">
       <textarea name="comment"></textarea>
       <?php
           require_once 'recaptchalib.php';
           echo recaptcha_get_html(PUBLICKEY);
       ?>
       <input type="submit" />
</form>


当用户提交表单时,您同样具有一个可以验证他们是否成功识别 CAPTCHA 文本的函数:

<?php
       require_once 'recaptchalib.php';
       $captcha = recaptcha_check_answer(PRIVATEKEY, 
           $_SERVER["REMOTE_ADDR"], 
           $_POST["recaptcha_challenge_field"],
           $_POST["recaptcha_response_field"]);
    if ($captcha->is_valid) {
           // Successful captcha, process the submission
       } else {
           // Invalid, offer them the CAPTCHA again.
       }
   ?>


如果您设置了防火墙,则需要修改 recaptchalib.php 以使用代理。请参见非官方 reCAPTCHA wiki 上的示例。

需要注意的是,虽然大多数人将 CAPTCHA 看作图像中的字母,但我看到许多其他实现方式得以成功使用。一种方式是一组猫脸图片中包含一只狗,您需要从中找出狗。另一种方式涉及简单的数学问题:仅要求用户计算像 2+3 这样的问题的答案,并将答案输入域中。(虽然一个脚本就可以解决此问题,但需要根据您网站的具体情况进行定制改写。)我甚至看到过仅要求您输入博主名字的 CAPTCHA。答案从未更改,但对博客而言,它大大减少了垃圾信息。

实施 CAPTCHA 解决方案时,您还应记住许多解决方案的可访问性较差。reCAPTCHA 通过提供声音 CAPTCHA 解决了此问题。

需要用户代理

您可以做的最简单的事情之一就是需要用户代理标头。所有有效的 Web 浏览器都在它们发送到 Web 服务器的标头中包括一个用户代理字符串。但是,许多脚本都不愿麻烦地设置代理。实际上,PHP 默认情况下不设置用户代理,因此,由于这种省略,可以识别所有用 PHP 编写的基本垃圾信息脚本。

在 PHP 应用程序中,您只需阻止所有不包括 $_SERVER["HTTP_USER_AGENT"] 值的帖子,并直接停止这些帖子跟踪中的许多脚本。还可以在防火墙或负载平衡器中实现这种阻止。

CSS 隐藏域

另一个尝试捕获过于丰富的脚本的窍门是保留一个“honeypot 域”。具体做法是在 HTML 表单中有一个看似真实域的单独域,甚至具有与通常请求的信息匹配的名称属性,例如用户位置、网站等。然后,您通过 CSS 隐藏该域。例如:

<html>
     <head>
       <style>
         .honeypot { display: none; }
       </style>
     </head>
     <body>
       <form method="POST" action="/submission">
         <p>Comment: <textarea name="comment"></textarea></p>
         <p class="honeypot">URL: <input name="url" type="text" /></p>
         <p><input type="submit" /></p>
       </form>
     </body>
</html>


这意味着合法用户将不会看到该域,因此从不填写该域 — 而试图模仿人的脚本将填写每一个可能的域。在后端,您只需检查该域是否已填写。如果已填写,则可以认为提交是由脚本执行的。

应该指出,这种特定的测试不是很容易实现。如果有人使用禁用了 CSS 的浏览器或仅使用基于较旧文本的浏览器,实际上可能会看到该域并尝试填写该域。因此,该方法可能仅应用来指示可能的垃圾信息,而不是唯一的判定。

如果您查看上述示例,可能会注意到脚本具有足够的智能来查找任何具有“display:none;”的 CSS。为此,我看到使用这一特性的人们获得更多的 CSS 创意,他们仅通过更改 Z 层使字体颜色与背景一致,或者任何其他诸如此类的实际上使该域对人眼不可见,但计算机脚本难以检测到的方法,将文字隐藏起来。

需要 JavaScript

我不会过多地关注该方法,因为它是一种相当严厉的措施,会导致严重的后果。但是,另一种解决办法是将您的提交表单构建为需要运行 JavaScript。大多数脚本都没有内置完整的 JavaScript 引擎,因此无法将数据提交到您的服务器。

例如,通过 DOM 操作完全动态构建 <form>,根本不使用表单,但通过 DOM designMode 使用实时可编辑的 HTML 文本,或者更炫的方法,比如包括实时 JavaScript 数学问题,JavaScript 需要实时解答这些问题,然后答案作为提交过程一部分回传,看是否与服务器上的答案一致(CSRF 保护的一种复杂变体)。

所有这些解决方案都存在一个问题:虽然有效,但它们也会阻止在未启用 JavaScript 功能情况下进行浏览的任何人。可能更重要的是,如果由于某种原因而存在 JavaScript 错误(可能仅在特定浏览器上),它们将阻止每个人提交数据。

基于 IP 的解决方案

下一类解决方案的核心是使用黑名单。在这些情况下,如果用户的 IP 地址符合“已知不良”主机的黑名单,您将完全阻止对您网站的所有访问(至少完全阻止对提交过程的访问)。这是阻止特定用户向您发送垃圾信息的有效方法。

公开的黑名单

公开提供了许多由其他人维护的黑名单,您可以通过编程方式进行访问。应该指出,这些列表通常针对电子邮件垃圾信息发送者(而不是网站垃圾信息发送者)设计。但是,通常如果主机用于某种负面活动,安全的做法是假设其他活动也是负面的。

您与每个黑名单连接的方式有所不同,本文不对此进行详细介绍。最常用的两个黑名单是 SpamCopSpamHaus。您还可以在 DNSBL.info 中找到各种服务器的大列表。虽然某些服务是免费提供的,但它们通常仅在作为电子邮件过滤系统的一部分使用时才免费。直接访问原始列表以将其用作反评论垃圾信息措施是需要花钱的。

作为一个简单的示例,您可以通过对 IP 地址执行反向 DNS 查找(后跟“.bl.spamcop.net”),根据 SpamCop 数据库检查任何 IP 地址。您可以使用以下代码:

<?php
   function spamIP($ip) {
       $reversed = preg_replace('/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/', '$4.$3.$2.$1', $ip);
       return checkdnsrr("{$reversed}.bl.spamcop.net", 'ANY');
   }
   ?>


如果您从 SpamCop 获得有效响应,则表示 SpamCop 已将这些地址加入黑名单。

自定义黑名单

虽然使用公开的黑名单可以在已知垃圾信息发送者到达之前将其捕获,但如果垃圾信息发送者仅将您的站点作为目标,则公开的黑名单根本没有任何帮助。对此,最好的解决方案是保存您自己的阻止访问的垃圾信息发送者黑名单。

您究竟如何将项添加到该黑名单取决于您的网站。在需要的时候通过 SQL 手动添加这些项会更容易吗?还是您具有可以用于针对黑名单标记 IP 的管理网站?MySQL 中的一个示例黑名单表可能如下所示:

CREATE TABLE `blacklist` ( 
    `id`         INTEGER(11) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `addr`       INTEGER(11) UNSIGNED NOT NULL,
    `expiration` DATETIME NOT NULL 
   ) ENGINE=innodb DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;


您将注意到这个简单表的一个非常重要添加项:“expiration”列。您从未永远阻止 IP 地址,这一点非常重要。IP 地址是临时的。只因为一个人在此刻使用该 IP 地址,并不意味着同一人从现在起一年内(甚至从现在起五分钟内)都使用该 IP 地址。对于 ISP 尤其如此,其中某人每次连接到互联网时,他们可能会被授予池中的不同 IP 地址。

将诸如 1.2.3.4 之类的 IP 地址添加到该表时,您只需运行:

INSERT INTO `blacklist` SET
     `addr` = INET_ATON('1.2.3.4'),
     `expiration` = DATE_ADD(NOW(), INTERVAL 1 WEEK);


在该示例中,我们将到期时间设置为未来一周之后。您可以根据自己的需要修改该时间。一种常用的策略是具有滚动系统。因此,第一次阻止 IP 地址时,您可能仅阻止一小时。第二次阻止一天。此后,转为阻止一周。这可能是阻止 IP 地址的最长时间。

根据已创建的黑名单进行检查只是基于日期的简单查找:

SELECT count(*) from `blacklist`
     WHERE `addr` = INET_ATON('1.2.3.4') AND `expiration` > NOW();


扫描内容

此时,所有方法术都关注如何阻止垃圾信息发送者,而没有考虑内容。如果这些方法失败或者不起作用,您实际上需要开始扫描内容本身以查看它是否为垃圾信息。这可能有些棘手,因为您现在实际上有可能获得误报结果,而拒绝有效结果,原因是您所使用的服务恰巧认为内容看起来可疑。

有许多服务可帮助您扫描垃圾信息。其中一些服务专门基于特定平台(用于 Wordpress 的 AntiSpamBee),其他服务在一个程序包中提供许多选项,如 Mollom,不仅可以防止垃圾评论,而且还提供 CAPTCHA 解决方案。您甚至只需重新设置提交的格式使其看似电子邮件并传递提交,即可改变某些邮件扫描解决方案(如 SpamAssassin)的用途。最后,这些只是存在的众多解决方案中的几个,一些互联网搜索将阻止其他人浏览。与此同时,让我们更详细地探讨一个常用解决方案 Akismet。

Akismet

Akismet 这个产品最早作为 Wordpress 上的垃圾信息解决方案启动,由 Automattic 创建。它在多年前推出,具有通用的强健 API。更重要的是,它针对个人使用是免费的,针对商业使用也非常便宜。

人们已提供了许多用来与 Akismet 连接的库,但其 API 文档齐全并使用基于 REST 的简单访问。在网站上注册获得免费 API 密钥之后,可以使用以下示例代码检查垃圾信息:

<?php

$APIkey = 'MY_API_KEY';
$commentData = array(
    'blog' => 'http://myapp.example.com/',
    'user_ip' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?:
        $_SERVER['REMOTE_ADDR'],
    'user_agent' => $_SERVER['HTTP_USER_AGENT'],
    'referrer' => $_SERVER['HTTP_REFERER'],
    'permalink' => 'http://myapp.example.com/post/id/5767',
    'comment_type' => 'comment',
    'comment_author' => 'Martha Jones',
    'comment_author_email' => 'mj@example.com',
    'comment_author_url' => 'http://martha.example.com/',
    'comment_content' => 'Loved this article.  Thanks for writing it!',
    );

$options = array(
    'http' => array(
        'method' => 'POST',
        'user_agent' => 'TestApp/0.9 | Akismet/1.11',
        'header' => "Content-Type: application/x-www-form-urlencoded",
        'content' => http_build_query($commentData)
    )
);
$ctxt = stream_context_create($options);
$result = file_get_contents(
          "http://{$APIkey}.rest.akismet.com/1.1/comment-check", false, $ctxt);
$isSpam = ($result === 'true');

if ($isSpam) {
    // Treat the comment as spam
} else {
    // Accept the comment as valid
}

?>


您可以看到 PHP“注意到”是否在环境中未设置 $_SERVER 数组值 — 将在生产代码中对此进行测试。如果您设置了防火墙,则将 $option['http']['proxy'] 设置为互联网代理。

使用 Akismet 之类的系统的一个最大好处是它始终在学习和改进 — 不仅从其众多用户那里学习,而且还可以了解您自己网站上的具体趋势。API 提供了多种方法,有助于对其进行培训以更好地满足您的特定需求。提供了“submit-spam”和“submit-ham”端点,您可以在这些端点提交 comment-check 端点所需的完全相同的信息,但服务器分别将这些数据标记为无效或有效。通过这种方式,您可以向它通知它所产生的误报以及它所漏掉的垃圾信息。

使垃圾信息用处更少

到目前为止,我们尝试了甚至在垃圾信息发送者向我们发送垃圾信息之前将其捕获,我们还探讨了如何在过后扫描数据以查看它是否为垃圾信息。您可以采取的另一项措施是使到达您系统的所有垃圾信息用处更少。如果向您的系统发送垃圾信息无论如何都不会对垃圾信息发送者有实际帮助,最终垃圾信息发送者将停止。(或至少一个垃圾信息发送者希望如此!)

rel="nofollow"

为了减少垃圾信息的用处,一种常用的的策略是向发布到您网站的任何链接添加 rel="nofollow" 属性。这是 Google 在多年前提出的一个概念,现在大多数搜索引擎都遵循此概念。简单地说,通过将该属性添加到任何 <a href> 标记,搜索引擎不会为因此链接的网站提供任何额外的优先权。传统上,与另一个网站链接的网站数量是理解站点相关性的极佳统计数据。垃圾信息发送者试图通过向其他网站注入链接来利用这一点。

关于该属性的使用存在一些争议。通常,应用该属性会影响所有链接,这也意味着有用、相关的链接也不再具有额外的吸引力。因此,虽然惩治了垃圾信息发送者,而合法使用也受到牵连。

禁止链接

下一步只需禁止提交中包含的所有链接。这无疑会阻止垃圾信息发送者,因为他们无法将人们重定向到其网站。但是,这显然也会严重影响合法用户。此外,它仍无法阻止所有垃圾信息。您可能使垃圾信息发送者仅输入文本链接并希望某人将这些链接剪切/粘贴到其浏览器中。您仍然还会收到“品牌”垃圾信息,其全部目的是推销特定产品,因此它们不担心包含链接。

总结

总的说来,您现在希望有非常好的工具集供您使用,以打击您网站上的垃圾信息。此外,您应该谨慎使用该工具集,选择最适用于您自己情况的工具,并了解哪些工具对您的合法用户影响最小。

为此,我有一些想法,希望与大家讨论。在如何应用这些方法方面,具有许多变化。例如,如果您检测到某些内容为垃圾信息,您应该公然拒绝它还是默默处理它?在前一种情况下,您可以明确声明此处不接受垃圾信息,但您也可能会给某人带来查找过滤器的麻烦。在后一种情况下,您可能使提交看似有效,但仅针对该用户 — 其他任何人都无法看到它。在这种情况下,允许垃圾信息发送者徒劳地发送垃圾信息。当然,它不会消除垃圾信息发送者。

同样,您应该立即对内容运行所有过滤器吗?这意味着您的用户需要等待这些检查完成(有时很慢)。还是您让所有内容都通过,并在过后对垃圾信息进行批处理?后者使您的网站具有高性能,但您能够忍受出现垃圾信息(即使是五分钟)吗?

您还可能要考虑跟踪用户随时间变化的行为记录的想法。这样做 — 让用户证明他们自己 — 可允许您使用相当严格的过滤器,但向已知的良好用户公开这些过滤器。例如,最初需要对评论进行适度控制,但在您批准某用户发布的一个帖子之后,不会控制该用户以后的所有帖子。或者如果某帐户存在时间超过一年,则信任该帐户。CAPTCHA 很适合使用行为记录,特定用户或用户会话连续成功完成三次之后,也许不提供 CAPTCHA。归根结底,无论是脚本还是人,只要他们能够连续三次分辨出来,就能继续进行分辨。

最后提醒您一句,这也是我的经验之谈:注意可改变的内容。如果您允许用户在发布提交内容之后编辑提交内容,则他们会高兴地提交有效评论,然后将评论更改为垃圾信息。因此,您需要通过相同的过滤器检查评论的每一次反复。同样,还要额外注意内容中的任何 URL。某 URL 今天指向有效、相关的网站,明天可能会通过重定向到某一隐藏位置而更改。



Eli White
长期使用 PHP,他是《PHP 5 in Practice》一书的作者。他参与过许多大型 PHP 项目,包括 Digg、TripAdvisor 和哈勃空间望远镜项目。他经常在 PHP 会议上发表演讲分享知识。有关 Eli 的更多信息,请访问 eliw.com