您的登录页面安全吗?

您认为一个Web应用程序为了安全地登录用户需要满足多少个标准?《Web应用程序黑客手册》(http://www.amazon.com/gp/product/1118026470/ref=as_li_qf_sp_asin_il_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1118026470&linkCode=as2&tag=perltrickscom-20)(联盟链接)列出了5个

  1. 防止信息泄露
  2. 秘密处理凭证
  3. 正确验证凭证
  4. 防止暴力攻击
  5. 记录、监控和通知

所以有5个标准,但您是如何实现它们的?我创建了一个名为“SecApp”的新Web应用程序,使用Perl的Catalyst Web框架来尝试满足这些标准 - 我们将逐一检查,您可以自己判断它是否做到了。

如何设置应用程序

如果您想下载应用程序并跟随,您当然可以,但这不是必须的。您至少需要Perl 5.14.4和一个安装的git。要从我们的GitHub页面下载应用程序,只需打开命令行并输入

$ git clone https://github.com/dnmfarrell/SecApp_login

别无选择;这个应用程序有很多依赖项。为了减轻负担,首先在命令行中安装cpanminus

$ cpan App::cpanminus

我更喜欢在安装大量模块时使用cpanminus:它比cpan消耗的内存更少,默认情况下输出的行噪声更少,并且具有有用的“–notest”选项,如果您想在不测试模块的情况下安装模块(并节省大量时间)。现在切换到新克隆的应用程序目录,并使用cpanminus安装应用程序的依赖项

$ cd SecApp_login
$ cpanm --installdeps .
--> Working on .
Configuring SecApp-0.01 ... OK
<== Installed dependencies for .. Finishing.

“–installdeps”选项指示cpanminus在当前目录中搜索依赖项。应用程序的所有依赖项都列在Makefile.PL中,因此cpanminus找到这些并开始安装应用程序所需的但系统尚未安装的所有Perl模块。如果您正在使用Perl的新安装,这可能需要一个小时左右,所以您可以泡杯咖啡或做其他事情,同时安装在进行。

一旦所有模块都安装完毕,使用以下命令测试运行应用程序

$ TESTING=1 script/secapp_server.pl 
HTTP::Server::PSGI: Accepting connections at http://0:3000/

打开您的浏览器并导航到http://localhost:3000。您会看到这个简单的欢迎信息

如果您访问http://localhost/login,它应该加载登录页面

使用用户名“test_user_01”和“Hfa *-£(&&%HBbWqpV%”_=asd”您应该可以登录。

成功登录将显示一条简单消息和注销链接

1. 防止信息泄露

信息泄露会给潜在的攻击者提供削弱登录安全性的线索。他们这样做的一种方式是提供有关运行Web应用程序的软件的信息(这些软件可能存在已知弱点)。

在SecApp中,我已经关闭了典型的Catalyst信息泄露。在根应用程序文件SecApp.pm中,已删除“-Debug”插件,该插件在发生错误时打印完整的堆栈跟踪

use Catalyst qw/
    Static::Simple
    Authentication
    Session
    Session::Store::File
    Session::State::Cookie
/;

在同一文件的下部,通过修改包配置已禁用了“X-Catalyst”HTTP头。这阻止了将头插入到每个响应中

# Disable X-Catalyst header
enable_catalyst_header => 0,

这两个更改阻止了应用程序告知用户底层应用程序框架和语言。现在他们不会知道他们是在处理一个Ruby、Python还是Perl应用程序!

我们需要防止的其他类型的信息泄露是通过对不同请求以不同的方式响应来指示逻辑漏洞。例如,通过向具有不正确用户名的登录尝试返回错误消息“不正确的用户名”,攻击者可以暴力攻击用户名,直到他们收到消息“不正确的密码”,这时他们就知道他们猜对了正确的用户名。

在SecApp中,我们希望在登录尝试失败时返回一个通用的消息,而不指出哪个字段不正确。登录功能在我们的Root.pm控制器中实现 - 我们稍后将会查看代码,但现在你可以看到只返回了一个错误消息。

2. 秘密处理凭据

网络应用黑客手册》将这一点总结为:

所有凭据应以不导致未授权披露的方式创建、存储和传输。

在SecApp的Root.pm中,我们使用Catalyst的auto Controller函数来检查每个请求是否通过SSL

# this method will be called everytime
sub auto :Private {
    my ($self, $c) = @_;

    # 404 unless https/testing & request method is GET/HEAD/POST
    unless( ( $c->req->secure or $c->config->{testing} == 1 )
            && grep /^(?:GET|HEAD|POST)$/, $c->req->method )
        {
            $c->detach('default');
        }
    ...
    return 1;
}

方法“$c->req->secure”如果连接是通过SSL返回true。如果不是,我们将请求连接到“默认”方法,返回404请求错误。语句“或$c->config->{testing} == 1”是为了在测试应用程序时,我们可以尝试这些功能而不需要SSL,因为Catalyst的测试服务器不支持SSL。

现在,对于那些尝试加载登录页面并收到404错误的用户来说可能很烦恼。因此,我们使用Catalyst的end方法,还设置了Strict-Transport-Security HTTP头,指示浏览器通过https加载所有页面。这是代码:

sub end : ActionClass('RenderView') {
  my ($self, $c) = @_;

  # don't require TLS for testing
  unless ($c->config->{testing} == 1) {
    $c->response->header('Strict-Transport-Security' => 'max-age=3600');
  }
  ...
}

SecApp在end方法中设置了几个其他安全头,你可以在这里了解它们的作用。

SecApp只认证通过POST接收到的登录请求。我们通过使用Catalyst的链式调度和HTTP方法匹配来实现这一点

sub login :Chained('/') PathPart('login') CaptureArgs(0) {}

sub login_auth :Chained('login') PathPart('') Args(0) POST {
    # authentication code
    ...

    # authentication failed, load the login form
    $c->forward('login_form');
}

sub login_form :Chained('login') PathPart('') Args(0) GET {
    my ($self, $c) = @_;

    # load the login template
    $c->stash(template => 'login.tt');
    ...
}

这里为了清晰起见,对代码进行了简略。但实质上,“login_auth”子程序只有在请求“/login”是通过POST方式发送时才会触发,否则只是加载带有“login_form”子的登录页面。酷吧?Catalyst项目经理John Napiorkowski在一篇说明性的博客文章中思考了这些特性。

最后,SecApp以散列格式存储密码,使用相对强大的算法(bcrypt)。在User.pm中的以下代码添加了功能

__PACKAGE__->add_columns(
            'password' => {
                passphrase => 'rfc2307',
                passphrase_class => 'BlowfishCrypt',
                passphrase_args => {
                    cost => 14,
                    salt_random => 20,
                },
                passphrase_check_method => 'check_password',
            });

因此,即使攻击者获得了应用程序密码文件,密码也会被加盐和散列,而且不容易破解。SecApp附带了包含一个已创建的测试用户账户的样本SQLite3测试数据库。

3. 正确验证凭据

验证凭据的代码也可能存在弱点。密码应完整验证,不进行修改或截断,并执行大小写敏感的比较。多阶段登录过程尤其容易受到攻击。登录代码应进行同行评审,并大量测试以发现错误。

Catalyst::Plugin::Authentication模块使认证变得简单。SecApp保持登录过程简单:仅有一个用户名和密码表单,可选的CAPTCHA。以下是完整的登录代码

sub login_auth :Chained('login') PathPart('') Args(0) POST {
  my ($self, $c) = @_;
  my $captcha_response 
    = $c->request->params->{recaptcha_response_field};
  my $captcha_challenge 
    = $c->request->params->{recaptcha_challenge_field};

  # proceed if config has switched off CAPTCHA, or if the submission is valid, proceed
  if ($c->config->{Captcha}->{enabled} == 0
      || Captcha::reCAPTCHA->new->check_answer(
                   $c->config->{Captcha}->{private_key},
                   $c->request->address,
                   $captcha_challenge,
                   $captcha_response)->{is_valid})
  {
    $username = $c->req->params->{username};
    my $password = $c->req->params->{password};

    # if username and passwords were supplied, authenticate
    if ($username && $password) {
      if ($c->authenticate({ username => $username,
                             password => $password } ))
      {
      # authentication success, check user active and redirect to the secure landing page
        if ($c->user->get_object->active) {
          $c->response->redirect($c->uri_for($c->controller('Admin')->action_for('landing')));
          return;
        }
      }
      else {
        $c->stash(error_msg => "Bad username or password.");
      }
    }
  }
  $c->forward('login_form');
}

让我们一步步查看代码。如果启用CAPTCHA功能,登录函数将尝试验证CAPTCHA。如果成功,代码将检索用户名和密码,如果它们存在,则尝试使用authenticate方法验证它们。authenticate方法会与数据库中的用户名和密码进行完整匹配。如果用户名和密码验证成功,则用户将被重定向到位于安全Admin.pm控制器的着陆页。否则,将设置错误消息,指示用户名或密码错误。在所有失败的案例中,登录表单将被重新加载并显示。

所以代码看起来不错,但我们如何知道它在所有情况下都能做到正确的事情呢?幸运的是,Catalyst::Test可以使应用程序方法的单元测试变得简单。SecApp有测试文件Root.t,该文件使用许多不同的凭证组合测试登录函数,例如空、零长度字符串、正确的用户名错误的密码等。运行这些测试可以很容易地确认登录函数是否做得正确。你想自己检查吗?在命令行运行

$ TESTING=1 perl -Ilib t/Root.t

4. 防止暴力攻击

暴力攻击是通过重复尝试不同的组合来破解账户的用户名和密码的尝试。SecApp使用Captcha::reCAPTCHA来防止自动化的暴力攻击。你需要一个谷歌账户和网站域名来注册(它是免费的)。提供的CAPTCHA难题的难度非常大,自动化很难可靠地通过。如果你有谷歌reCAPtCHA账户,你可以通过更新SecApp.pm中的账户凭据在SecApp中试用。

鉴于暴力攻击只有在它们可以尝试数百万次的情况下才能成功,为什么不在登录函数中添加时间延迟,比如“sleep(2)”呢?这种防御方法的问题在于它将网络应用程序暴露给另一个攻击向量:拒绝服务。如果攻击者可以每2秒向登录函数发出几个请求,它可能会占用应用程序的所有进程,阻止它对常规网络请求做出响应。这不好!

结合使用CAPTCHA和前端代理网络服务器请求和连接限制方法,可以很大程度上消除暴力攻击的风险。

6. 日志、监控和通知

Catalyst自带内置的日志记录功能。如果你使用Catalyst::Plugin::Authentication,任何失败的登录尝试都会自动记录一个关键错误。所以好消息是,如果你使用像nginx这样的网络服务器,Catalyst会将关键错误写入服务器错误日志(这是一个简化)。SecApp没有实现任何监控或通知服务,但我认为这更多是服务器而不是网络应用程序的领域。配置fail2ban来监控error.log并拘留任何可疑的重复登录尝试是微不足道的。

结论

SecApp登录函数安全吗?需要考虑的一点是,尽管它利用了许多良好的做法,但用户注册和密码重置尚未实现。这些功能也必须是安全的,否则它们可能会完全削弱登录安全性,例如允许设置弱密码。我们将在未来的文章中考虑这些认证领域的。同时,SecApp在Artistic 2.0许可下发布,请随意使用。

喜欢这篇文章吗?帮我们一下,推文关于它!

*更新:更正了散列算法名称和描述 04/28/2014*

封面图像 © motograf


本文最初发布在 PerlTricks.com

标签

David Farrell

David是一位专业程序员,他经常在推特博客上发表关于代码和编程艺术的帖子。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。