防止跨站脚本攻击

简介

跨站脚本攻击是目前面临的最常见且常被忽视的安全问题之一。如果一个网站在没有检查恶意脚本标签的情况下显示用户提交的内容,则该网站是脆弱的。

幸运的是,Perl和mod_perl为我们提供了解决这个问题的简单方法。我们强调这些内置的解决方案,并介绍了一个新的mod_perl模块:Apache::TaintRequest。此模块通过将Perl的强大“污染”规则应用于HTML输出,帮助您保护mod_perl应用程序。

什么是“跨站脚本”?

最近新闻报道充斥着关于网站安全漏洞的报道。一些最近的标题包括以下令人不安的内容:《安全漏洞使微软钱包面临风险》,《Schwab金融网站容易受到攻击》,《新型黑客攻击对流行Web服务构成威胁》。在这些所有情况下,根本问题都是由跨站脚本攻击引起的。这种攻击不是针对服务器操作系统的漏洞或Web服务器软件,而是直接针对您网站的用户的。它通过欺骗用户向目标网站上的动态表单提交Web脚本代码(JavaScript、Jscript等)来实现。如果网站没有检查这种脚本代码,它可能会将其原封不动地传回用户的浏览器,从而造成各种损害。

考虑以下URL

http://www.example.com/search.pl?text=<script>alert(document.cookie)</script>

如果攻击者能让我们选择这样的链接,并且Web应用程序没有验证输入,那么我们的浏览器会弹出警报,显示我们当前的cookie集合。这个特定的例子是无害的;攻击者可以造成更大的损害,包括窃取密码、重置主页或将您重定向到另一个网站。

更糟糕的是,您甚至可能不需要选择链接就能发生这种情况。如果攻击者能让您的应用程序显示一段HTML,那么您就麻烦了。`IMG`和`IFRAME`标签允许在显示HTML时加载新的URL。例如,以下HTML片段是由BadTrans蠕虫发送的。这个蠕虫使用由`IFRAME`标签提供的加载在视图上的功能来感染运行Outlook和Outlook Express的系统。

  --====_ABC1234567890DEF_====
  Content-Type: multipart/alternative;
           boundary="====_ABC0987654321DEF_===="

  --====_ABC0987654321DEF_====
  Content-Type: text/html;
           charset="iso-8859-1"
  Content-Transfer-Encoding: quoted-printable


  <HTML><HEAD></HEAD><BODY bgColor=3D#ffffff>
  <iframe src=3Dcid:EA4DMGBP9p height=3D0 width=3D0>
  </iframe></BODY></HTML>
  --====_ABC0987654321DEF_====--

  --====_ABC1234567890DEF_====
  Content-Type: audio/x-wav;
           name="filename.ext.ext"
  Content-Transfer-Encoding: base64
  Content-ID: <EA4DMGBP9p>

这个特定的例子会在目标计算机上运行可执行代码。攻击者可以同样容易地使用前面描述的URL格式插入HTML,如下所示

<iframe src="http://www.example.com/search.pl?text=<script>alert(document.cookie)</script>">

在处理Web浏览器对cookie的内部限制时,“跨站”部分在跨站脚本攻击中起作用。现代Web浏览器内建的JavaScript解释器只允许原始站点访问其自己的私有cookie。通过利用编写不良的脚本,攻击者可以绕过这个限制。

任何编写在Perl或其他语言中的编写不良的脚本都是潜在的目标。解决跨站脚本攻击的关键是永远不要信任来自Web浏览器的数据。任何输入数据都应该被认为是有罪的,除非证明它是无辜的。

解决方案

对于Perl和mod_perl系统,有几种方法可以解决这个问题。所有这些方法都很简单,应该在任何可能存在用户提交数据出现在结果网页上的地方使用。

考虑以下脚本search.pl。它是一个简单的CGI脚本,它接受一个名为‘text’的参数,并在屏幕上打印它。

        #!/usr/bin/perl
        use CGI;

        my $cgi = CGI->new();
        my $text = $cgi->param('text');

        print $cgi->header();
        print "You entered $text";

此脚本容易受到跨站脚本攻击,因为它盲目地打印出提交的表单数据。为了消除这种漏洞,我们可以执行输入验证,或者确保在显示之前,用户提交的数据始终是HTML转义的。

我们可以在任何输出之前插入以下代码来为我们的脚本添加输入验证。此代码从提交的输入中消除除字母、数字和空格之外的所有内容。

        $text =~ s/[^A-Za-z0-9 ]*/ /g;

这种类型的输入验证可能相当麻烦。另一种解决方案是转义提交数据中的任何HTML。我们可以通过使用libwww-perl CPAN分发中捆绑的HTML::Entities模块来完成此操作。HTML::Entities模块提供了HTML::Entities::encode()函数。它将HTML字符编码为HTML实体引用。例如,字符<转换为<转换为",依此类推。以下是一个使用此新功能的search.pl版本。

        #!/usr/bin/perl
        use CGI;
        use HTML::Entities;

        my $cgi = CGI->new();
        my $text = $cgi->param('text');

        print $cgi->header();
        print "You entered ", HTML::Entities::encode($text);

mod_perl的解决方案

所有这些解决方案也适用于mod_perl程序员。Apache::Registry脚本或mod_perl处理器可以使用相同的技巧来消除跨站脚本漏洞。为了获得更高的性能,您可能希望考虑将调用从HTML::Entities::encode()切换到mod_perl的更快函数Apache::Util::escape_html()。以下是一个与前面的search.pl脚本等效的Apache::Registry脚本示例。

        use Apache::Util;
        use Apache::Request;

        my $apr = Apache::Request->new(Apache->request);

        my $text = $apr->param('text');

        $r->content_type("text/html");
        $r->send_http_header;
        $r->print("You entered ", Apache::Util::html_encode($text));

过了一段时间后,您可能会发现重复输入Apache::Util::html_encode()变得相当麻烦,尤其是如果您在某些地方使用输入验证,而在其他地方没有使用。为了简化这种情况,请考虑使用Apache::TaintRequest模块。此模块可以从CPAN或从mod_perl开发者手册网站获取。

Apache::TaintRequest自动执行HTML转义数据的繁琐过程。它覆盖了mod_perl Apache模块中的打印机制。新的print方法检查每个文本块是否受污染。如果受污染,则该模块假定最坏的情况,并在打印之前将其HTML转义。

Perl包含一组称为受污染模式的内置安全检查。这些检查通过确保来自程序外部的受污染数据不会直接或间接地用于更改文件、进程或目录来保护您。Apache::TaintRequest扩展了包括将HTML打印到Web客户端在内的危险操作列表。要清除您的数据,只需使用正则表达式处理它即可。受污染是Perl网络开发者的最强大的安全防护。请查阅perlsec手册页,并在您编写的每个Web应用程序中使用它。

要激活Apache::TaintRequest,只需将以下指令添加到您的httpd.conf中。

       PerlTaintCheck on    

这将为整个mod_perl服务器激活受污染模式。

接下来,我们需要修改我们的脚本或处理器以使用Apache::TaintRequest而不是Apache::Request。前面的脚本可能看起来像这样

        use Apache::TaintRequest;

        my $apr = Apache::TaintRequest->new(Apache->request);

        my $text = $apr->param('text');

        $r->content_type("text/html");
        $r->send_http_header;

        $r->print("You entered ", $text);

        $text =~ s/[^A-Za-z0-9 ]//;
        $r->print("You entered ", $text);

此脚本首先将受污染的表单数据‘text’存储在$text中。如果我们打印此数据,我们会发现它被自动HTML转义。接下来我们对数据进行输入验证。以下打印语句不会导致数据发生任何HTML转义。

受污染 + Apache::Request… Apache::TaintRequest

Apache::TaintRequest的实现代码非常简单。它是一个Apache::Request模块的子类,它提供了表单字段和输出处理。我们覆盖了print方法,因为这是我们HTML转义数据的地方。我们还覆盖了new方法——这是我们在使用Apache的TIEHANDLE接口来确保输出到STDOUT被我们的print()例程处理的地方。

一旦我们有了输出数据,我们需要确定它是否受污染。这时,污染模块(也可从CPAN获取)就变得很有用。我们在print方法中使用它来确定可打印的字符串是否受污染,需要被HTML转义。如果受污染,我们使用Apache::Util::html_escape()函数来转义HTML。

package Apache::TaintRequest;

use strict;
use warnings;

use Apache;
use Apache::Util qw(escape_html);
use Taint qw(tainted);

$Apache::TaintRequest::VERSION = '0.10';
@Apache::TaintRequest::ISA = qw(Apache);

sub new {
  my ($class, $r) = @_;

  $r ||= Apache->request;

  tie *STDOUT, $class, $r;

  return tied *STDOUT;
}


sub print {
  my ($self, @data) = @_;

  foreach my $value (@data) {
    # Dereference scalar references.
    $value = $$value if ref $value eq 'SCALAR';

    # Escape any HTML content if the data is tainted.
    $value = escape_html($value) if tainted($value);
  }

  $self->SUPER::print(@data);
}

要完成这个模块,我们只需要在我们的new()方法中指定的TIEHANDLE接口。以下代码实现了TIEHANDLEPRINT方法。

sub TIEHANDLE {
  my ($class, $r) = @_;

  return bless { r => $r }, $class;
}

sub PRINT {
  shift->print(@_);
}

最终结果是受污染的数据被转义,未受污染的数据则未经修改地传递给网络客户端。

结论

跨站脚本是一种严重的问题。解决方案,输入验证和HTML转义很简单,但必须每次都应用。即使只有一个被忽视的表单字段,应用程序的安全性也和完全没有检查一样。

为了确保我们始终检查我们的数据,开发了Apache::TaintRequest。它通过在打印时自动HTML转义未经过输入验证的数据,基于Perl强大的数据污染特性。

资源

标签

反馈

这篇文章有什么问题吗?请在GitHub上打开一个issue或pull request来帮助我们GitHub