§防止跨站请求伪造
跨站请求伪造 (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 功能都假设在隐式作用域中存在一个隐式 RequestHeader
(或一个 Request
,它扩展了 RequestHeader
),如果没有,则无法编译。以下将显示示例。
§在操作中定义隐式请求
对于所有需要访问 CSRF 令牌的操作,必须使用 `implicit request =>` 隐式公开请求,如下所示
// this actions needs to access CSRF token
def someMethod: Action[AnyContent] = Action { implicit request =>
// access the token as you need
Ok
}
这是因为像 CSRF.getToken
这样的辅助方法访问接收请求作为隐式参数来检索 CSRF 令牌,例如
def someAction: Action[AnyContent] = Action { implicit request =>
accessToken // request is passed implicitly to accessToken
Ok("success")
}
def accessToken(implicit request: Request[_]) = {
val token = CSRF.getToken // request is passed implicitly to CSRF.getToken
}
§在方法之间传递隐式请求
如果您将代码分解为使用 CSRF 功能的方法,那么您可以从操作中传递隐式请求。
def action: Action[AnyContent] = Action { implicit request =>
anotherMethod("Some para value")
Ok
}
def anotherMethod(p: String)(implicit request: Request[_]) = {
// do something that needs access to the request
}
§在模板中定义隐式请求
您的 HTML 模板应该有一个隐式 RequestHeader
参数传递给您的模板,如果它还没有,因为 CSRF.formField
助手需要一个传递进来(下面将详细讨论)。
@(...)(implicit request: RequestHeader)
由于您通常会将 CSRF 与需要 MessagesProvider
实例的表单助手一起使用,您可能希望使用 MessagesAbstractController
或其他提供 MessagesRequestHeader
的控制器。
@(...)(implicit request: MessagesRequestHeader)
或者,如果您使用的是具有 I18nSupport
的控制器,您可以将消息作为单独的隐式参数传递。
@(...)(implicit request: RequestHeader, messages: Messages)
§获取当前令牌
可以使用 CSRF.getToken
方法访问当前 CSRF 令牌。它接受一个隐式 RequestHeader
,因此请确保作用域中有一个。
val token: Option[CSRF.Token] = CSRF.getToken
注意:如果安装了 CSRF 过滤器,Play 将尝试避免生成令牌,只要使用的 cookie 是 HttpOnly(这意味着它无法从 JavaScript 访问)。当发送具有严格主体的响应时,Play 会跳过将令牌添加到响应中,除非已经调用了 `CSRF.getToken`。这对于不需要 CSRF 令牌的响应来说,可以显著提高性能。如果 cookie 未配置为 HttpOnly,Play 将假设您希望从 JavaScript 访问它,并无论如何都会生成它。
如果您没有使用 CSRF 过滤器,您也应该注入 CSRFAddToken
和 CSRFCheck
操作包装器,以强制在特定操作上添加令牌或 CSRF 检查。否则,令牌将不可用。
import play.api.mvc._
import play.api.mvc.Results._
import play.filters.csrf._
import play.filters.csrf.CSRF.Token
class CSRFController(components: ControllerComponents, addToken: CSRFAddToken, checkToken: CSRFCheck)
extends AbstractController(components) {
def getToken =
addToken(Action { implicit request =>
val Token(name, value) = CSRF.getToken.get
Ok(s"$name=$value")
})
}
为了帮助将 CSRF 令牌添加到表单中,Play 提供了一些模板助手。第一个将它添加到操作 URL 的查询字符串中
@import helper._
@form(CSRF(routes.ItemsController.save())) {
...
}
这可能会呈现一个看起来像这样的表单
<form method="POST" action="/items?csrfToken=1234567890abcdef">
...
</form>
如果在查询字符串中使用令牌不可取,Play 还提供了一个助手,用于将 CSRF 令牌作为隐藏字段添加到表单中
@form(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 提供了两个可以与您的应用程序操作组合的操作。
第一个操作是 CSRFCheck
操作,它执行检查。它应该添加到所有接受会话身份验证的 POST 表单提交的操作中
import play.api.mvc._
import play.filters.csrf._
def save = checkToken {
Action { implicit req =>
// handle body
Ok
}
}
第二个操作是 CSRFAddToken
操作,它在传入请求中不存在时生成 CSRF 令牌。它应该添加到所有呈现表单的操作中
import play.api.mvc._
import play.filters.csrf._
def form = addToken {
Action { implicit req => Ok(views.html.itemsForm) }
}
应用这些操作的一种更便捷的方法是将它们与 Play 的 操作组合 结合使用
import play.api.mvc._
import play.filters.csrf._
class PostAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = checkToken(action)
}
class GetAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = addToken(action)
}
然后,您可以最大限度地减少编写操作所需的样板代码
def save: Action[AnyContent] = postAction {
// handle body
Ok
}
def form: Action[AnyContent] = getAction { implicit req => Ok(views.html.itemsForm) }
§CSRF 配置选项
可以在过滤器 reference.conf 中找到完整的 CSRF 配置选项范围。一些示例包括
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
如果您的应用程序使用编译时依赖注入,则可以使用上述所有功能。您可以将特性 CSRFComponents 混合到您的应用程序组件蛋糕中,以帮助进行连接。有关编译时依赖注入的更多详细信息,请参阅 相关文档页面。
§测试 CSRF
渲染时,您可能需要将 CSRF 令牌添加到模板中。您可以使用 import play.api.test.CSRFTokenHelper._
来完成此操作,它会用 withCSRFToken
方法丰富 play.api.test.FakeRequest
import play.api.test.CSRFTokenHelper._
import play.api.test.FakeRequest
import play.api.test.Helpers._
import play.api.test.WithApplication
class UserControllerSpec extends Specification {
"UserController GET" should {
"render the index page from the application" in new WithApplication() {
override def running() = {
val controller = app.injector.instanceOf[UserController]
val request = FakeRequest().withCSRFToken
val result = controller.userGet().apply(request)
status(result) must beEqualTo(OK)
contentType(result) must beSome("text/html")
}
}
}
}
下一步: 自定义验证
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。