首先判断 session 中有没有 csrftoken,如果没有,则认为是第一次访问,session 是新建立的,这时生成一个新的 token,放于 session 之中,并继续执行请求。如果 session 中已经有 csrftoken,则说明用户已经与服务器之间建立了一个活跃的 session,这时要看这个请求中有没有同时附带这个 token,由于请求可能来自于常规的访问或是 XMLHttpRequest 异步访问,我们分别尝试从请求中获取 csrftoken 参数以及从 HTTP 头中获取 csrftoken 自定义属性并与 session 中的值进行比较,只要有一个地方带有有效 token,就判定请求合法,可以继续执行,否则就转到错误页面。生成 token 有很多种方法,任何的随机算法都可以使用,Java 的 UUID 类也是一个不错的选择。
除了在服务器端利用 filter 来验证 token 的值以外,我们还需要在客户端给每个请求附加上这个 token,这是利用 js 来给 html 中的链接和表单请求地址附加 csrftoken 代码,其中已定义 token 为全局变量,其值可以从 session 中得到。
清单 3. 在客户端对于请求附加 token
function appendToken(){
updateForms();
updateTags();
}
function updateForms(){
// 得到页面中所有的 form 元素
var forms =document.getElementsByTagName('form');
for(i=0;i
var url =forms[i].action;
// 如果这个 form 的 action 值为空,则不附加 csrftoken
if(url ==null||url =="")continue;
// 动态生成 input 元素,加入到 form 之后
var e =document.createElement("input");
e.name ="csrftoken";
e.value =token;
e.type="hidden";
forms[i].appendChild(e);
}
}
function updateTags(){
var all =document.getElementsByTagName('a');
var len =all.length;
// 遍历所有 a 元素
for(var i=0;i
var e =all[i];
updateTag(e,'href',token);
}
}
function updateTag(element,attr,token){
var location =element.getAttribute(attr);
if(location !=null&&location !=''''){
var fragmentIndex =location.indexOf('#');
var fragment =null;
if(fragmentIndex !=-1){
//url 中含有只相当页的锚标记
fragment =location.substring(fragmentIndex);
location =location.substring(0,fragmentIndex);
}
var index =location.indexOf('?');
if(index !=-1){
//url 中已含有其他参数
location =location +'&csrftoken='+token;
}else{
//url 中没有其他参数
location =location +'?csrftoken='+token;
}
if(fragment !=null){
location +=fragment;
}
element.setAttribute(attr,location);
}
}
在客户端 html 中,主要是有两个地方需要加上 token,一个是表单 form,另一个就是链接 a。这段代码首先遍历所有的 form,在 form 最后添加一隐藏字段,把 csrftoken 放入其中。然后,代码遍历所有的链接标记 a,在其 href 属性中加入 csrftoken 参数。注意对于 a.href 来说,可能该属性已经有参数,或者有锚标记。因此需要分情况讨论,以不同的格式把 csrftoken 加入其中。
如果你的网站使用 XMLHttpRequest,那么还需要在 HTTP 头中自定义 csrftoken 属性,利用 dojo.xhr 给 XMLHttpRequest 加上自定义属性代码如下:
清单 4. 在 HTTP 头中自定义属性
var plainXhr =dojo.xhr;// 重写 dojo.xhr 方法
dojo.xhr =function(method,args,hasBody){
// 确保 header 对象存在
args.headers =args.header ||{};
tokenValue ='<%=request.getsession(false).getattribute("csrftoken")%>';%=request.getsession(false).getattribute("csrftoken")%>
<%=request.getsession(false).getattribute("csrftoken")%>var token =dojo.getObject("tokenValue");
// 把 csrftoken 属性放到头中
args.headers["csrftoken"]=(token)?token :"";
returnplainXhr(method,args,hasBody);
};
这里改写了 dojo.xhr 的方法,首先确保 dojo.xhr 中存在 HTTP 头,然后在 args.headers 中添加 csrftoken 字段,并把 token 值从 session 里拿出放入字段中。
PHP代码示例:
请看下面一个简单的应用,它允许用户购买钢笔或铅笔。界面上包含下面的表单:
Item:
pen
pencil
Quantity:
下面的buy.php程序处理表单的提交信息:
session_start();
$clean =array();
if(isset($_REQUEST['item']&&isset($_REQUEST['quantity']))
{
/* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */
if(buy_item($clean['item'],$clean['quantity']))
{
echo '< session_start();
$clean =array();
if(isset($_REQUEST['item']&&isset($_REQUEST['quantity']))
{
/* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */
if(buy_item($clean['item'],$clean['quantity']))
{
echo '< session_start();
$clean =array();
if(isset($_REQUEST['item']&&isset($_REQUEST['quantity']))
{
/* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */
if(buy_item($clean['item'],$clean['quantity']))
{
echo '
Thanks for your purchase.
';
}
else
{
echo '
There was a problem with your order.
';
}
}?>
攻击者会首先使用这个表单来观察它的动作。例如,在购买了一支铅笔后,攻击者知道了在购买成功后会出现感谢信息。注意到这一点后,攻击者会尝试通过访问下面的URL以用GET方式提交数据是否能达到同样的目的:
http://store.example.org/buy.php?item=pen&quantity=1
如果能成功的话,攻击者现在就取得了当合法用户访问时,可以引发购买的URL格式。在这种情况下,进行跨站请求伪造攻击非常容易,因为攻击者只要引发受害者访问该URL即可。
请看下面对前例应用更改后的代码:
php
session_start();
$token =md5(uniqid(rand(),TRUE));
$_SESSION['token']=$token;
$_SESSION['token_time']=time();?>
表单:
Item:
pen
pencil
Quantity:
通过这些简单的修改,一个跨站请求伪造攻击就必须包括一个合法的验证码以完全模仿表单提交。由于验证码的保存在用户的session中的,攻击者必须对每个受害者使用不同的验证码。这样就有效的限制了对一个用户的任何攻击,它要求攻击者获取另外一个用户的合法验证码。使用你自己的验证码来伪造另外一个用户的请求是无效的。
该验证码可以简单地通过一个条件表达式来进行检查:
if(isset($_SESSION['token'])&&$_POST['token']==$_SESSION['token'])
{
/* Valid Token */
}?> if(isset($_SESSION['token'])&&$_POST['token']==$_SESSION['token'])
{
/* Valid Token */
}?> if(isset($_SESSION['token'])&&$_POST['token']==$_SESSION['token'])
{
/* Valid Token */
}?>
你还能对验证码加上一个有效时间限制,如5分钟:
$token_age =time()-$_SESSION['token_time'];
if($token_age < $token_age =time()-$_SESSION['token_time'];
if($token_age < $token_age =time()-$_SESSION['token_time'];
if($token_age <=300)
{
/* Less than five minutes has passed. */
}?>
通过在你的表单中包括验证码,你事实上已经消除了跨站请求伪造攻击的风险。可以在任何需要执行操作的任何表单中使用这个流程。
%=request.getsession(false).getattribute("csrftoken")%>(责任编辑:安博涛)