【五 form提交及校验】 2. 防御CSRF攻击

跨站请求伪造(CSRF)是一种安全漏洞,攻击者利用受害者的 session 来通过受害者的浏览器发出请求。攻击者通过受害者的浏览器发送请求,并伪造成是受害者自己发出的请求。

建议你先熟悉CSRF,哪些是相关的攻击向量而哪些不是。我们建议从这里开始。

很难直接区分哪些请求是安全的而哪些请求容易被CSRF攻击,这是因为没有对插件及未来扩展的明确规范。因为历史遗留原因,浏览器的插件及扩展放宽了信任规则,引入了CSRF这样的漏洞,现在修复漏洞的任务就落到了框架头上。因此Play默认采取了保守的方案,但允许开发者自由修改策略。默认情况下,当下列所有情况全部满足时Play会要求做CSRF检查:

  • 请求方法不是 GET,HEAD 或者 OPTIONS
  • 请求包含一个或多个 Cookie 或者 Authorization 头
  • CORS 过滤器没有配置为信任请求源(request's origin)

注意:如果你使用了基于浏览器的身份验证,如NTLM或者客户端证书,而非基于cookies的或者HTTP的身份验证,那么你必须将 play.filters.csrf.header.protectHeaders设置为null,或者在protectHeaders中将身份验证的请求头包含进来。

Play的CSRF防护

Play支持多种方式来校验一个请求是否是CSRF请求。最优先的方式就是使用CSRF token。此token在query string或请求体中随每次请求一起提交,并在用户session中保存。Play每次都会校验这两个token是否存在及是否匹配。

为了对那些非浏览器的请求做简单保护,Play仅仅在请求头中检查cookies。如果请求是AJAX提交的,你可以将CSRF token保存在html页面中,然后将它放到Csrf-Token中提交。

或者你可以这样设置 play.filters.csrf.header.bypassHeaders:

  • 如果设置了 X-Requested-With 头,Play就将请求视为安全的。很多JS 库都支持添加 X-Request-With ,如JQuery。
  • 如果 Csrf-Token 头的值为 nocheck,或者提供了一个有效的 CSRF token,Play就将请求视为安全的。

具体的设置方法如下:

play.filters.csrf.header.bypassHeaders {
  X-Requested-With = "*"
  Csrf-Token = "nocheck"
}

在使用这种方式来防御CSRF攻击时应该格外谨慎,因为它会被历史遗留的浏览器插件破坏。

信任CORS请求

默认如果在CSRF 过滤器之前存在CORS过滤器,CSRF过滤器将允许特定来源的CORS请求。要禁用这种检查,可以将 play.filters.csrf.bypassCorsTrustedOrigins 设置为 false。

使用全局CSRF过滤器

注意:在Play2.6.x中,CSRF过滤器在Play的默认过滤器调用链中。要查看更多过滤器信息请点击这里

Play提供了全局CSRF过滤器来过滤所有的请求。这也是为项目添加CSRF防御的最简单方式。也可以这样来手动添加它:

play.filters.enabled += "play.filters.csrf.CSRFFilter"

也可以通过在路由前添加nocsrf修饰符来为特定的路由禁用CSRF过滤:

+ nocsrf
POST  /api/new              controllers.Api.newThing

使用隐式请求

所有的CSRF功能都默认已经提供了一个默认的隐式 RequestHeader(也可以使用Request,它继承了RequestHeader),当没有找到时,它将不会编译。下面会展示具体的例子。

在Action中定义隐式Request

对于所有需要回去CSRF token的action来说都需要一个隐式的request:

// this actions needs to access CSRF token
def someMethod = Action { implicit request =>
    // access the token as you need
    Ok
  }

这么做的原因是因为 CSRF.getToken 需要从request中获取到 CSRF token,下面的代码展示了这个过程:

def someAction = 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
}

在方法间传递隐式的Request

如果你需要在自己的方法中使用 CSRF 相关功能,你也可以使用 action 中隐式的request:

def action = Action { implicit request =>
  anotherMethod("Some para value")
  Ok
}

def anotherMethod(p: String)(implicit request: Request[_]) = {
  // do something that needs access to the request
}

在模板中定义隐式Requests

如果HTML模板没有RequestHeader,你就必须提供一个隐式的。因为 CSRF.formField 需要用到:

@(...)(implicit request: RequestHeader)

CSRF需要和form helper一起使用,而form helper本身又需要一个 MessagesProvider。这时可以考虑使用 MessagesAbstractController 或其他提供MessagesRequestHeader的controller:

@(...)(implicit request: MessagesRequestHeader)

又或者如果你使用了I18nSupport,那么可以传入如下的隐式参数:

@(...)(implicit request: RequestHeader, messages: Messages)

获取当前 token

当前token可以通过 CSRF.getToken方法获取到,请确保在当前作用域中存在一个隐式的 RequestHeader

val token: Option[CSRF.Token] = CSRF.getToken

注意:如果配置了CSRF过滤器,而且cookie设置为HttpOnly(意味着JavaScript无法获取到cookie),那么Play将避免生成token。服务端发送带有strict body的response时,Play会跳过向response添加token的步骤,除非 CSRF.getToken方法已经被·调用。这将带来大幅的性能提升。如果cookie没有被配置为HttpOnly的,Play将认为你需要在JavaScript中访问token,因此始终会生成token。

如果你并没有使用 CSRF 过滤器,那么需要手动注入 CSRFAddToken 和 CSRFCheck action wrapper来为特定action强制添加CSRF检测,否则token将不可用。

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")
  })
}

Play也提供了一些列的helper来简化向form添加token的步骤。首先来看看通过action URL的query string添加:

@import helper._

@form(CSRF(routes.ItemsController.save())) {
    ...
}

上边的代码会渲染出的form是这样的:

<form method="POST" action="/items?csrfToken=1234567890abcdef">
   ...
</form>

如果不希望在查询字符串中有令牌,Play还提供了一个在表单中添加隐藏CSRF令牌字段的Helper:

@form(routes.ItemsController.save()) {
    @CSRF.formField
    ...
}

这种方式生成的form如下所示:

<form method="POST" action="/items">
   <input type="hidden" name="csrfToken" value="1234567890abcdef"/>
   ...
</form>

向session添加CSRF token 

为了确保 CSRF token 随 form 发送到用户端,如果传入的请求中还没有token,全局过滤器将为所有的 GET 请求生成一个新的token。

为特定的 action 添加过滤器

还有一些不基于session的标准来应对那些全局CSRF过滤不适用的场景,如OpenID 2.0,可以使用在跨站的form表单提交,或者在服务器间的RPC通信中使用form提交。

Play提供了两个action,可以自由组合进你的应用程序action中。

第一个 CSRFCheck action 用于检测,你应该在所有接受session鉴权的form提交请求action中加上它:

import play.api.mvc._
import play.filters.csrf._

def save = checkToken {
  Action { implicit req =>
    // handle body
    Ok
  }
}

第二个是 CSRFAddToken,如果传入请求中还没有出现CSRF token,它就生成一个新的CSRF token。它应该添加到所有渲染表单的action中:

import play.api.mvc._
import play.filters.csrf._

def form = addToken {
  Action { implicit req =>
    Ok(views.html.itemsForm)
  }
}

可以如下这样来简单将它们组合进 Play 的action:

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)
}

然后通过一点简单的模板代码就可以创建action:

def save = postAction {
  // handle body
  Ok
}

def form = getAction { implicit req =>
  Ok(views.html.itemsForm)
}

CSRF配置项

关于CSRF的全部配置项可以在这里查看,下边是一些例子:

  • play.filters.csrf.token.name —— 在会话和请求体/Query String中使用的token的名称。默认为csrfToken。
  • play.filters.csrf.cookie.name —— 如果配置,Play将把CSRF token存储在指定名称的cookie中,而不是session。
  • play.filters.csrf.cookie.secure —— 在已经设置了play.filters.csrf.cookie.name属性的情况下,CSRF cookie是否应该设置安全标志。默认与play.http.session.secure设置相同。
  • play.filters.csrf.body.bufferSize —— 为了从body中读取token,Play必须首先缓冲body并解析。可以用此项配置来设置缓冲区大小。默认为100 k。
  • play.filters.csrf.token.sign —— Play 是否使用签名的CSRF token。签名token确保每个请求的token值是随机的,从而防止BREACH 攻击。

在编译时依赖注入CSRF

如果应用程序使用的是编译时依赖注入,可以在应用程序组件中混入 trait CSRFComponents使用上述所有特性。有关编译时依赖项注入的详细信息请参阅相关文档

测试CSTF

在渲染时,可以利用import play.api.test.CSRFTokenHelper._ 来向模板中添加token。这里的 withCSRFToken 方法扩充了 play.api.test.FakeRequest的功能:

import play.api.test.Helpers._
import play.api.test.CSRFTokenHelper._
import play.api.test.{ FakeRequest, WithApplication }

class UserControllerSpec extends Specification {
  "UserController GET" should {

    "render the index page from the application" in new WithApplication() {
      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")
    }
  }
}

 

 

转载于:https://my.oschina.net/landas/blog/2987690

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值