抵御跨站伪造请求
https://playframework.com/documentation/2.6.x/JavaCsrf
CSRF是一个安全漏洞,攻击者通过受害者的浏览器在会话期间发起一个请求。由于每一个请求都会带有session token,如果攻击者能够迫使被害者浏览器以自己的身份发出请求,那么也能以用户的名义发出请求。
建议你了解以下CSRF,了解一下什么是攻击向量什么不是,可以通过这里熟悉CSRF
对于什么是安全的请求没有一个简单的答案,因为目前还没有一个明确的规则说明插件和未来的扩展可以做什么( the reason for this is that there is no clear specification as to what is allowable from plugins and future extensions to specifications. )历史上,浏览器插件和扩展项会将规则放宽,认为框架是可以信赖的,这将CSRF威胁带到了许多应用中,然后由框架来修复这些漏洞。基于这个原因,Play采取了保守的策略,但是当允许你配置什么时候进行检查。默认情况下,当以下条件成立时Play会采取CSRF检测
- GET、HEAD、OPTIONS请求
- 请求中包了一个或多个Cookier或Authorization头
- CORS 过滤器没有被配置为相信请求origin
Note:如果你是用基于浏览器的认证而不是cookies或者HTTP认证,比如NTLM或者客户端证书。那么你必须设置play.filters.csrf.header.protectHeaders = null,或者将在protectHeaders中使用的验证头部包含进来 |
Play的CSRF防御
Play提供多种方法来验证一个请求是否是CSRF。首要的机制是一个CSRF token。在提交表单或者query string都会带有这个Token,也保存在用户会话中。Play会验证双方的token是否一致。
为了简单的保护非浏览器请求,Play仅检查带有cookies的请求。如果通过Ajax来创建请求,你可以将CSRF token放到HTML页面中,然后通过Csrf-Token头降到请求中。
另外,可以通过设置play.filters.csrf.header.bypassHeaders来匹配普通的头部,一般规则如下:
- 如果有一个X-Requested-With头,Play会认为这个请求安全。X-Requested-With是很多常用js库添加的。
- 如果一个Csrf-Token头部的值为nocheck,或者有一个合法的CSRF token,则认为请求安全
配置如下
play.filters.csrf.header.bypassHeaders {
X-Requested-With = "*"
Csrf-Token = "nocheck"
}
使用如上配置的时候需要小心, as historically browser plugins have undermined this type of CSRF defence.
相信CORS请求
默认情况下,如果在CSRF过滤器前有一个CORS过滤器,CSRF过滤器会让CORS请求通过。如果想要禁用这,可以配置play.filters.csrf.bypassCorsTrustedOrigins = false.
使用全局的CSRF过滤器
Note: 从Play2.6开始,CSRF过滤器会是Play的默认过滤器之一。 |
Play提供了一个全局的CSRF过滤器来处理所有的请求,这是添加CSRF防御最简单的方法。可以通过以下方式手动添加到application.conf中
play.filters.enabled += "play.filters.csrf.CSRFFilter"
对于一些特殊路偶也可以禁用过滤,在routes文件中添加`nocsrf
标签
+ nocsrf
POST /api/new controllers.Api.newThing()
获取当前的token
可以通过CSRF.getTOken方法来获取,这个方法需要RequestHeader参数,可以在Http.Context.current() 通过context.request()来获取
Optional<CSRF.Token> token = CSRF.getToken(request());
Note: 如果CSRF过滤器已经安装,当cokkie只被HTTP使用(也就说说JS无法获取)Play会试着避免生成token。当使用strick body发送响应时,Play会跳过添加token的过程除非CSRF.getToken。这在一些不需要CSRF token的相应中可以显著提高效率。如果cookie没有被配置为HttpOnly,Play会认为J你希望JS能够获取到cookie信息。 Note:如果你通过CompletionStage 来获得模版并得到了一个There is no HTTP Context的错误,那么你需要添加了一个HttpExecutionContext.current(),详见https://playframework.com/documentation/2.6.x/JavaAsync |
为了给表单增加CSRF token,Play提供了一下模板工具,首先添加到action URL的查询字符串中。
@import helper._
@form(CSRF(scalaguide.forms.csrf.routes.ItemsController.save())) {
...
}
表单大致是这个样子:
<form method="POST" action="/items?csrfToken=1234567890abcdef">
...
</form>
如果不想在查询字符串中获取token,Play也支持提供一个helper在form中添加一个隐藏域来存放token
@form(scalaguide.forms.csrf.routes.ItemsController.save()) {
@CSRF.formField
...
}
<form method="POST" action="/items">
<input type="hidden" name="csrfToken" value="1234567890abcdef"/>
...
</form>
为会话添加CSRF token
为了保证表单中CSRF token可用且能反给客户端,如果新来的请求中token不可用,全局的过滤器会为所有获取HTML的GET请求生成一个新的token。
为一个action添加CSRF过滤器
有些情景下全局的CSRF过滤器并不适用,比如有一些应用也许希望通过一些跨源的表单请求(for example in situations where an application might want to allow some cross origin form posts.)一些不基于会话的标准,OpenID2.0,要求跨站的表单请求,或者提供RPC调用的表单提交。
在这些场景中,Play提供了两个action可以组合到我们的action中。
第一个是 play.filters.csrf.RequireCSRFCheck ,他会执行一个CSRF检查。这个action需要被添加到所有接收会话验证的POST表单提交的action中。
@RequireCSRFCheck
public Result save() {
// Handle body
return ok();
}
第二个是 play.filters.csrf.AddCSRFToken,如果接收的请求没有token就会生成一个。他需要被添加到所有render forms的action中
@AddCSRFToken
public Result get() {
return ok(CSRF.getToken(request()).map(CSRF.Token::value).orElse("no token"));
}
CSRF可选配置
CSRF的所以配置可以在reference.conf 中找到。
- play.filters.csrf.token.name-用在会话和请求体/查询字符串中的名字
- play.filters.csrf.cookie.name- 如果配置了,Play会在cookie中根据配置的名字保存token,会话中将不再保存
- play.filters.csrf.cookie.secure- 如果上一项配置了,CSRF cookie是否需要安全flag set。 默认使用play.http.session.secure的值
- play.filters.csrf.body.bufferSize - 为了在body之外读取token,Play需要对bodu进行缓冲并根据情况进行解析。这一项设置了buffer的最大值,默认为00K
- play.filters.csrf.token.sign - Play是否使用签名的CSRF token。签名的CSRF token保证token的值在每次请求时都是随机的,因此可以抵御BREACH style攻击。
测试CSRF
在功能测试中,如果你为一个模板返回了CSRF token,你需要使这个token可用。可以在一个play.mvc.Http.RequestBuilder实例上调用play.api.test.CSRFTokenHelper.addCSRFToken
Http.RequestBuilder request = new Http.RequestBuilder()
.method(POST)
.uri("/xx/Kiwi");
request = CSRFTokenHelper.addCSRFToken(request);