§防止跨站点请求伪造
跨站点请求伪造 (CSRF) 是一种安全漏洞,攻击者利用它欺骗受害者的浏览器使用受害者的会话发出请求。由于会话令牌会随每个请求一起发送,如果攻击者可以强制受害者的浏览器代表他们发出请求,攻击者就可以代表用户发出请求。
建议您熟悉 CSRF,了解攻击向量是什么以及攻击向量不是什么。我们建议您从 OWASP 的此信息 开始。
对于哪些请求是安全的以及哪些容易受到 CSRF 请求的攻击,没有简单的答案,原因是对于插件和未来扩展规范允许的内容没有明确的规范。从历史上看,浏览器插件和扩展程序放宽了框架以前认为可以信任的规则,从而导致许多应用程序出现 CSRF 漏洞,而框架则需要负责修复这些漏洞。出于这个原因,Play 在其默认设置中采取了保守的方法,但允许您配置何时进行检查。默认情况下,Play 将在以下所有条件都为真时要求进行 CSRF 检查
- 请求方法不是
GET
、HEAD
或OPTIONS
。 - 请求包含一个或多个
Cookie
或Authorization
标头。 - CORS 过滤器未配置为信任请求的来源。
注意:如果您使用基于浏览器的身份验证,而不是使用 cookie 或 HTTP 身份验证(例如 NTLM 或基于客户端证书的身份验证),那么您必须设置
play.filters.csrf.header.protectHeaders = null
,这将保护所有请求,或者将身份验证中使用的标头包含在protectHeaders
中。
§Play 的 CSRF 防护
Play 支持多种方法来验证请求是否为 CSRF 请求。主要机制是 CSRF 令牌。此令牌放置在提交的每个表单的查询字符串或主体中,并且也放置在用户的会话中。然后,Play 验证这两个令牌是否存在并匹配。
为了允许对非浏览器请求进行简单的保护,Play 默认情况下会检查带有 Cookie
或 Authorization
标头的请求。您可以配置 play.filters.csrf.header.protectHeaders
来定义必须存在的标头才能执行 CSRF 检查。如果您使用 AJAX 发出请求,则可以将 CSRF 令牌放置在 HTML 页面中,然后使用 Csrf-Token
标头将其添加到请求中。
或者,您可以设置 play.filters.csrf.header.bypassHeaders
来匹配常见的标头:常见的配置是
- 如果存在
X-Requested-With
标头,Play 将认为请求是安全的。X-Requested-With
由许多流行的 Javascript 库(如 jQuery)添加到请求中。 - 如果存在值为
nocheck
的Csrf-Token
标头,或者存在有效的 CSRF 令牌,Play 将认为请求是安全的。
此配置将如下所示
play.filters.csrf.header.bypassHeaders {
X-Requested-With = "*"
Csrf-Token = "nocheck"
}
使用此配置选项时应谨慎,因为历史上浏览器插件破坏了这种类型的 CSRF 防御。
§信任 CORS 请求
默认情况下,如果您在 CSRF 过滤器之前有一个 CORS 过滤器,则 CSRF 过滤器将允许来自受信任来源的 CORS 请求通过。要禁用此检查,请设置配置选项 play.filters.csrf.bypassCorsTrustedOrigins = false
。
§应用全局 CSRF 过滤器
注意:从 Play 2.6.x 开始,CSRF 过滤器包含在 Play 的默认过滤器列表中,这些过滤器会自动应用于项目。有关更多信息,请参见 过滤器页面。
Play 提供了一个全局 CSRF 过滤器,可以将其应用于所有请求。这是向应用程序添加 CSRF 防护的最简单方法。要手动添加过滤器,请将其添加到 application.conf
中
play.filters.enabled += "play.filters.csrf.CSRFFilter"
也可以在路由文件中为特定路由禁用 CSRF 过滤器。为此,请在路由之前添加 nocsrf
修饰符标签
+ nocsrf
POST /api/new controllers.Api.newThing()
§获取当前令牌
可以使用 CSRF.getToken
方法访问当前的 CSRF 令牌。它需要一个 RequestHeader
Optional<CSRF.Token> token = CSRF.getToken(request);
注意:如果安装了 CSRF 过滤器,Play 会尝试避免生成令牌,只要使用的 cookie 是 HttpOnly(这意味着它无法从 JavaScript 访问)。当发送具有严格正文的响应时,Play 会跳过将令牌添加到响应中,除非已经调用了
CSRF.getToken
。这对于不需要 CSRF 令牌的响应来说,可以显著提高性能。如果 cookie 未配置为 HttpOnly,Play 会假设您希望从 JavaScript 访问它,并无论如何都会生成它。
为了帮助将 CSRF 令牌添加到表单中,Play 提供了一些模板助手。第一个将它添加到操作 URL 的查询字符串中
@import helper._
@form(CSRF(scalaguide.forms.csrf.routes.ItemsController.save())) {
...
}
这可能会渲染一个看起来像这样的表单
<form method="POST" action="/items?csrfToken=1234567890abcdef">
...
</form>
如果在查询字符串中使用令牌不可取,Play 还提供了一个助手,用于将 CSRF 令牌作为隐藏字段添加到表单中
@form(scalaguide.forms.csrf.routes.ItemsController.save()) {
@CSRF.formField
...
}
这可能会渲染一个看起来像这样的表单
<form method="POST" action="/items">
<input type="hidden" name="csrfToken" value="1234567890abcdef"/>
...
</form>
§将 CSRF 令牌添加到会话中
为了确保 CSRF 令牌可用于在表单中渲染并发送回客户端,全局过滤器将为所有接受 HTML 的 GET 请求生成一个新令牌,如果传入请求中还没有令牌。
§按操作方式应用 CSRF 过滤
有时全局 CSRF 过滤可能不合适,例如在应用程序可能希望允许某些跨域表单发布的情况下。一些非基于会话的标准,例如 OpenID 2.0,需要使用跨站点表单发布,或在服务器到服务器的 RPC 通信中使用表单提交。
在这些情况下,Play 提供了两个可以与您的应用程序操作组合在一起的操作。
第一个操作是 play.filters.csrf.RequireCSRFCheck
操作,它执行 CSRF 检查。它应该添加到所有接受会话身份验证的 POST 表单提交的操作中
@RequireCSRFCheck
public Result save() {
// Handle body
return ok();
}
第二个操作是 play.filters.csrf.AddCSRFToken
操作,它会在传入请求中不存在 CSRF 令牌时生成一个。它应该被添加到所有渲染表单的操作中。
@AddCSRFToken
public Result get(Http.Request request) {
return ok(CSRF.getToken(request).map(CSRF.Token::value).orElse("no token"));
}
§CSRF 配置选项
完整的 CSRF 配置选项可以在过滤器 reference.conf 中找到。一些例子包括
play.filters.csrf.token.name
- 在会话和请求体/查询字符串中使用的令牌名称。默认值为csrfToken
。play.filters.csrf.cookie.name
- 如果配置,Play 会将 CSRF 令牌存储在具有给定名称的 cookie 中,而不是在会话中。play.filters.csrf.cookie.secure
- 如果设置了play.filters.csrf.cookie.name
,则 CSRF cookie 是否应该设置安全标志。默认值为与play.http.session.secure
相同的值。play.filters.csrf.body.bufferSize
- 为了从主体中读取令牌,Play 必须首先缓冲主体并可能解析它。这设置了用于缓冲主体的最大缓冲区大小。默认值为 100k。play.filters.csrf.token.sign
- Play 是否应该使用签名的 CSRF 令牌。签名的 CSRF 令牌确保令牌值在每次请求时都是随机的,从而阻止 BREACH 类型的攻击。
§测试 CSRF
在功能测试中,如果您正在渲染带有 CSRF 令牌的 Twirl 模板,则需要有一个可用的 CSRF 令牌。您可以通过在 play.mvc.Http.RequestBuilder
实例上调用 play.api.test.CSRFTokenHelper.addCSRFToken
来实现。
Http.RequestBuilder request = new Http.RequestBuilder().method(POST).uri("/xx/Kiwi");
request = CSRFTokenHelper.addCSRFToken(request);
下一步: 使用表单模板助手
发现此文档中的错误?此页面的源代码可以在 这里 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。