防止CSRF攻击

译者:韩国峰
本文已经获得原作者Email授权----译者注
概览:

1. Hello World
2. 介绍
3. 关于认证技术
3.1 Cookies Hashing
3.2 HTTP来路
3.3 验证码
4. 一次性令牌
5. 最后的话
1.Hello World

欢迎来到崭新的Playhack.net的新季度开题项目报告。我非常高兴您能够再次回来让我们的c001项目重现。

希望您能喜欢这个新的短篇论文,我邀请你浏览位于 http://www.playhack.net的全部新项目。

开始:几乎没有什么,只是一点香烟!:

呐喊:我向我的playhack m8s null,omni,god and emdel,ofc o str0ke大声呐喊!NEX 回来了。
2.介绍

我 对跨站请求伪造(Cross Site Request Forgery,即CSRF)技术有一定研究,但是对网站开发者应当采取的措施研究不深。这些日子在编写一个对用户和管理员(这些人对他们的任务并不明 晰:P)有高度安全要求的分布式网站程序时,我被这个话题深刻的纠缠了。

针对这种情况,我必须考虑程序最终可能受到的各个方面的可能的攻击威胁。

给我最多麻烦的就是Session欺骗(或者CSRF,你可以按照自己喜欢的方式称呼),因为这种攻击是完全以用户的身份,因此并没有百分百的可能性来防止它。

如果你对我刚才说所的Session欺骗并不太了解,那么你可以阅读: http://www.playhack.net/view.php?id=30
3.可行措施

Ok,从这里开始,我必须假定你对Session欺骗攻击的实施方法已经深刻领会了:P

让我们开始新的继续。

考虑到一个已经登录到网站的受信用户可以完成一些重要的或者私密的操作,攻击者尝试记性一个可能的登录攻击(但是大多数情况下是不可行的)并且得到已经登录用户的Session来实现其巧妙的行为。

为 了劫持用户的Seession,入侵者精心构造一个适当的网页,在这个网页中包含了隐藏的JavaScript函数来重新创造一个原始操作表单,但是攻击 者却修改了一些表单值,然后攻击者让受攻击者访问该页面,此时页面加载过程会提交上述表单到一个远程页面,以隐秘地完成一个请求(此时受攻击者并不知 道),他们用这种方法利用了用户的受信身份。

这种方式简单解释了Session欺骗攻击是如何工作的,但是一个重要的问题是,"我如何避免我的用户成为这种攻击的受害者?"

现在,你可能想到如下的几种方法:

检查Cookies凭据
检查HTTP请求来路
使用验证码
但是经过一些尝试,你会发现这些方法不是我们应当采取的最合适的解决方式,让我们一个个的来看为什么。
3.1 Cookies Hashing

第一个方案可能是解决这个问题的最简单和快捷的方案了,因为攻击者不能够获得被攻击者的Cookies内容,也就不能够构造相应的表单。

这个问题的实现方法与下面的类似。在某些登录页面我们根据当前的会话创建Cookies:
程序代码
<!-- login.php -->
<?php
// Cookie value
$value = "Something from Somewhere";
// Create a cookie which expires in one hour
setcookie("cookie", $value, time()+3600);
?>
<!-- EOF -->
在这里,我们在Cookies中使用了散列来使得这个表单可被认证。
程序代码
<!-- form.php -->
<?php
// Hash the cookie
$hash = md5($_COOKIE['cookie']);
?>
<form method="POST" action="resolve.php">
<input type="text" name="first_name">
<input type="text" name="last_name">
<input type="hidden" name="check" value="<?=$hash;?>">
<input type="submit" name="submit" value="Submit">
</form>
<!-- EOF -->

此时,后台的动态网页部分可以进行如下操作:
程序代码
<!-- resolve.php -->
<?php
// Check if the "check" var exists
if(isset($_POST['check'])) {
$hash = md5($_COOKIE['cookie']);
// Check if the values coincide
if($_POST['check'] == $hash) {
do_something();
} else {
echo "Malicious Request!";
}
} else {
echo "Malicious Request!";
}
?>
<!-- EOF -->
事 实上,如果我们不考虑用户的Cookies很容易由于网站中存在XSS漏洞而被偷窃(我们已经知道这样的事情并不少见)这一事实,这是一个很好的应对对 CSRF的解决方案。如果我们为用户的每一个表单请求中都加入随机的Cookies,那么这种方法会变得更加安全,但是这并不是十分合适。
3.2 HTTP来路

检 测访问来路是否可信的最简单方法是,获得HTTP请求中的来路信息(即名为Referer的HTTP头--译者注)并且检查它来自站内还是来自一个远程的恶 意页面:这是一个很好的解决方法,但是由于可以对服务器获得的请求来路进行欺骗以使得他们看起来合法,这种方法不能够有效防止攻击。

让我们来看看为什么这并不是一个合适的方法。

下面的代码展示了HTTP Referer实现方法的一个例子:
程序代码
<!-- check.php -->
if(eregi("www.playhack.net", $_SERVER['HTTP_REFERER'])) {
do_something();
} else {
echo "Malicious Request!";
}
<!-- EOF -->

这个检测则会轻易的忽略掉来自某个攻击者伪造的HTTP Referer欺骗,攻击者可以使用如下代码:

header("Referer: www.playhack.net");

或者其他在恶意脚本中伪造HTTP头并发送的方法。

由于HTTP Referer是由客户端浏览器发送的,而不是由服务器控制的,因此你不应当将该变量作为一个信任源。
3.3 验证码

另外一个解决这类问题的思路则是在用户提交的每一个表单中使用一个随机验证码,让用户在文本框中填写图片上的随机字符串,并且在提交表单后对其进行检测。

这个方法曾经在之前被人们放弃,这是由于验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。

你可以在Secunia的站点上获得关于此缺陷的详细信息: http://secunia.com/advisories/19738/

这里是Secunia关于此Bug解释的概述:

"此缺陷是由于处理"mhtml:"的URL处理器重定向引起的。它可以被用来利用从另外一个网站访问当前的文档"

在同一个页面你会找到来自Secunia工作人员的网站测试方法。

事实上,我们知道,这个Bug已经被微软放出的Windows XP和Windows Vista及其浏览器IE6.0的修复包所解决了。

即使他的确出现了安全问题,这么长时间也会有其他的可靠方案出现。

4.一次性令牌

现在让我们来看经过研究,我希望介绍的最后一种解决方案:在使用这些不可靠的技术后,我尝试做一些不同然而却是更有效的方法。

为了防止Web表单受到Session欺骗(CSRF)的攻击,我决定检测可能被伪装或伪造的每一个项目。因此我需要来创造一次性令牌,来使得在任何情况下都不能够被猜测或者伪装,这些一次性令牌在完成他们的工作后将被销毁。

让我们从令牌值的生成开始:
程序代码
<!-- start function -->
<?php
function gen_token() {
// Generate the md5 hash of a randomized uniq id
$hash = md5(uniqid(rand(), true));
// Select a random number between 1 and 24 (32-8)
$n = rand(1, 24);
// Generate the token retrieving a part of the hash starting from
// the random N number with 8 of lenght
$token = substr($hash, $n, 8);
return $token;
}
?>
<!-- EOF -->

PHP函数uniqid()允许web开发者根据当前的时间(毫秒数)获得一个唯一的ID,这个唯一ID有利于生成一个不重复的数值。

我们检索相应ID值的MD5散列,而后我们从该散列中以一个小于24的数字为开始位置,选取8位字母、

返回的$token变量将检索一个8位长的随机令牌。

现在让我们生成一个Session令牌,在稍后的检查中我们会用到它。
程序代码
<!-- start function -->
<?php
function gen_stoken() {
// Call the function to generate the token
$token = gen_token();
// Destroy any eventually Session Token variable
destroy_stoken();
// Create the Session Token variable
session_register(STOKEN_NAME);
$_SESSION[STOKEN_NAME] = $token;
}
?>
<!-- EOF -->
在这个函数中我们调用gen_token()函数,并且使用返回的令牌将其值复制到一个新的$_SESSION变量。

现在让我们来看启动完整机制中为我们的表单生成隐藏输入域的函数:

程序代码
<!-- start function -->
<?php
function gen_input() {
// Call the function to generate the Session Token variable
gen_stoken();
// Generate the form input code
echo "<input type=/"hidden/" name=/"" . FTOKEN_NAME . "/"
value=/"" . $_SESSION[STOKEN_NAME] . "/"> ";
}
?>
<!-- EOF -->
我们可以看到,这个函数调用了gen_stoken()函数并且生成在WEB表单中包含隐藏域的HTML代码。

接下来让我们来看实现对隐藏域中提交的Session令牌的检测的函数:

程序代码
<!-- start function -->
<?php
function token_check() {
// Check if the Session Token exists
if(is_stoken()) {
// Check if the request has been sent
if(isset($_REQUEST[FTOKEN_NAME])) {
// If the Form Token is different from Session Token
// it's a malicious request
if($_REQUEST[FTOKEN_NAME] != $_SESSION[STOKEN_NAME]) {
gen_error(1);
destroy_stoken();
exit();
} else {
destroy_stoken();
}
// If it isn't then it's a malicious request
} else {
gen_error(2);
destroy_stoken();
exit();
}
// If it isn't then it's a malicious request
} else {
gen_error(3);
destroy_stoken();
exit();
}
}
?>
<!-- EOF -->
这 个函数检测了$_SESSION[STOKEN_NAME]和$_REQUEST[FTOKEN_NAME]的存在性(我使用了$ _REQUEST方法来使得GET和POST两种方式提交的表单变量均能够被接受),而后检测他们的值是否相同,因此判断当前表单提交是否是经过认证授权 的。

这个函数的重点在于:在每次检测步骤结束后,令牌都会被销毁,并且仅仅在下一次表单页面时才会重新生成。

这些函数的使用方法非常简单,我们只需要加入一些PHP代码结构。

下面是Web表单:
程序代码
<!-- form.php -->
<?php
session_start();
include("functions.php");
?>
<form method="POST" action="resolve.php">
<input type="text" name="first_name">
<input type="text" name="last_name">
<!-- Call the function to generate the hidden input -->
<? gen_input(); ?>
<input type="submit" name="submit" value="Submit">
</FORM>
<!-- EOF -->

下面是解决的脚本代码:
程序代码
<!-- resolve.php -->
<?php
session_start();
include("functions.php");

// Call the function to make the check
token_check();

// Your code
...
?>
<!-- EOF -->
你可以看到,实现这样一个检测是十分简单的,但是它可以避免你的用户表单被攻击者劫持,以避免数据被非法授权。
5.结论

让我们对这篇简短的论文做一个结论,你的Web应用程序没有百分百的安全,但是你可以开始避免绝大多数普通的攻击技术。

我希望您关注的另一个要点是,Web开发者不应当忽视一般的程序错误(例如XSS 浏览器漏洞等等),不将这些考虑为对您的用户的潜在威胁是一个巨大的错误:你应该永远记得它们将影响程序的信任性、安全性和互操作性。

Cya!

nexus


还有LGN21ST的一篇
从Rails 2开始,默认使用cookie来保存session,于是很多关于安全上的置疑被频繁提及。尤其是cross-site request forgery(CSRF)攻击。

在Rails2中预防CSRF其实很容易,首先确保在application.rb中包含protect_from_forgery调用
程序代码
Ruby代码
class ApplicationController < ActionController::Base
protect_from_forgery
end

class ApplicationController < ActionController::Base
protect_from_forgery
end


至此,所有用form_for或者form_tag等生成的表单均被隐藏注入一个特别的基于用户session生成的token,在表单被提交时一并将token提交至服务器并接受服务器端的检查。

如果你象我一样喜欢自己手写Ajax Post请求代码,但是没有包含这个token的话,服务器端会报'verify_authenticity_token'错误,如何得到这个token呢?我的做法是在页面头部先得到这个token,赋值给一个JS的全局变量,在之后的Ajax代码中就可以直接引用啦,主意,一定要在Ajax调用代码之前作这一步,最保险的位置是在页面第一行。
程序代码
Ruby代码
<%= javascript_tag "var authenticity_token = '#{form_authenticity_token}';" %>

<%= javascript_tag "var authenticity_token = '#{form_authenticity_token}';" %>


Ajax代码大概可以这样写
程序代码
Ruby代码
new Ajax.Request(
request_path,
{method:'post',
parameters:'authenticity_token=' + encodeURIComponent(authenticity_token)});

你可能感兴趣的:(CSRF)