
Play Framework Scala CSRF 防护实战指南从全局过滤到按 Action 精确控制【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework导读CSRFCross Site Request Forgery跨站请求伪造是 Web 应用最危险的安全威胁之一攻击者利用受害者在目标站点已建立的会话诱导其浏览器代表自己发起请求从而在用户不知情的情况下执行操作。Play Framework 内置了完整的 CSRF 防护机制以保守且可配置的方式默认拦截可疑请求。本文以官方文档为骨架结合 Play 仓库中play-filters-helpers模块的源码实现系统讲解 Play 的 CSRF 防护原理、全局过滤器配置、表单与 AJAX 的令牌集成、按 Action 精确控制、完整配置项以及测试方法帮助 Scala 开发者构建既安全又灵活的 Web 应用。CSRF 攻击的本质与 Play 的防护策略Cross Site Request Forgery跨站请求伪造是一种安全漏洞攻击者欺骗受害者的浏览器利用受害者的会话发起请求。由于会话令牌session token会随每个请求自动携带一旦攻击者能让受害者的浏览器替自己发出请求就能以用户身份执行操作。OWASP 建议开发者先充分了解 CSRF 的攻击向量及其边界再实施防护。为什么安全请求的判定如此困难对于哪些请求是安全的、哪些存在 CSRF 风险业界并没有简单的答案因为浏览器插件与规范扩展对请求行为没有清晰的约束。历史上浏览器插件和扩展曾多次放宽框架先前认为可信任的规则导致大量应用引入 CSRF 漏洞最终只能由框架修复。因此 Play 采取保守的默认策略同时允许你精确配置何时执行检查。Play 默认的 CSRF 检查触发条件默认情况下Play 会在同时满足以下全部条件时执行 CSRF 检查请求方法不是GET、HEAD或OPTIONS请求带有至少一个Cookie或Authorization请求头CORS 过滤器未配置为信任该请求的来源origin。注意如果应用使用 Cookie 或 HTTP 认证以外的浏览器端认证方式例如 NTLM、基于客户端证书的认证则必须设置play.filters.csrf.header.protectHeaders null以保护所有请求或者将认证所用的请求头加入protectHeaders。Play 的 CSRF 防护机制令牌的生成与校验Play 支持多种方式验证请求不是 CSRF 请求核心机制是 CSRF 令牌token令牌被放入每个表单提交的查询字符串或请求体中同时被放入用户的 session或可配置的 cookie中Play 校验请求携带的令牌与会话中的令牌同时存在且匹配。非浏览器请求与 AJAX 的处理为简化非浏览器请求的防护Play 默认检查带有Cookie或Authorization请求头的请求。可通过play.filters.csrf.header.protectHeaders配置必须存在哪些请求头才执行 CSRF 检查。如果使用 AJAX 请求可以把 CSRF 令牌嵌入 HTML 页面再通过Csrf-Token请求头提交。通过 bypassHeaders 放行常见头部也可以配置play.filters.csrf.header.bypassHeaders匹配常见请求头从而跳过检查。一种常见配置请求带有X-Requested-With请求头时Play 认为请求安全——该头由 jQuery 等众多流行 JavaScript 库自动添加请求带有值为nocheck的Csrf-Token请求头或携带有效 CSRF 令牌时Play 认为请求安全。对应配置如下play.filters.csrf.header.bypassHeaders { X-Requested-With * Csrf-Token nocheck }使用该配置选项时必须谨慎因为历史上浏览器插件曾破坏这类 CSRF 防御手段。*仅表示存在该请求头即放行字符串值则表示精确匹配该值。信任 CORS 请求默认情况下如果应用在 CSRF 过滤器之前配置了 CORS 过滤器CSRF 过滤器会放行来自受信任来源的 CORS 请求。从源码看这一逻辑位于 CSRFActions.scala 的requiresCsrfCheck方法中当bypassCorsTrustedOrigins为true且请求带有CORSFilter.Attrs.Origin标记时直接跳过检查。如需禁用该放行设置play.filters.csrf.bypassCorsTrustedOrigins false应用全局 CSRF 过滤器注意自 Play 2.6.x 起CSRF 过滤器已包含在 Play 的默认过滤器列表中自动应用于项目参见 Filters 文档。Play 提供可应用于所有请求的全局 CSRF 过滤器这是为应用添加 CSRF 防护最简单的方式。若需手动添加在application.conf中配置play.filters.enabled play.filters.csrf.CSRFFilter在 reference.conf 中可以看到CSRF 过滤器与SecurityHeadersFilter、AllowedHostsFilter一起被列为默认启用的过滤器。全局过滤器对应的实现类为 CSRFFilter.scala它是一个EssentialFilter核心逻辑委托给CSRFAction。为特定路由禁用 CSRF也可以在 routes 文件中为特定路由禁用 CSRF 过滤器在路由前添加nocsrf修饰符标签参见 routes 示例 nocsrf POST /api/new controllers.Api.newThingnocsrf修饰符由play.filters.csrf.routeModifiers.whiteList [nocsrf]配置驱动见 reference.conf你可以改用其他修饰符名如api以便在代码中复用。使用隐式 Request 访问 CSRF 令牌所有 CSRF 功能都假定隐式作用域中有一个隐式的RequestHeader或其子类Request可用没有隐式 Request 将无法编译。下面给出示例。在 Action 中定义隐式 Request所有需要访问 CSRF 令牌的 Action 都必须通过implicit request 隐式暴露请求参见 ScalaCsrfController.scala// 这个 Action 需要访问 CSRF 令牌 def someMethod: Action[AnyContent] Action { implicit request // 按需访问令牌 Ok }这是因为CSRF.getToken等辅助方法通过隐式参数接收请求以检索 CSRF 令牌def someAction: Action[AnyContent] Action { implicit request accessToken // request 隐式传递给 accessToken Ok(success) } def accessToken(implicit request: Request[?]) { val token CSRF.getToken // request 隐式传递给 CSRF.getToken }在方法之间传递隐式 Request如果将代码拆分为多个方法且这些方法中使用了 CSRF 功能可以从 Action 向下传递隐式 requestdef action: Action[AnyContent] Action { implicit request anotherMethod(Some para value) Ok } def anotherMethod(p: String)(implicit request: Request[?]) { // 执行需要访问 request 的逻辑 }在模板中定义隐式 RequestHTML 模板应具备隐式的RequestHeader参数如果还没有的话因为CSRF.formField辅助方法要求传入隐式 RequestHeader(...)(implicit request: RequestHeader)由于通常会将 CSRF 与需要MessagesProvider实例的表单辅助方法结合使用可以改用MessagesAbstractController或其他提供MessagesRequestHeader的控制器(...)(implicit request: MessagesRequestHeader)或者如果控制器使用了I18nSupport可以把 messages 作为单独的隐式参数传入(...)(implicit request: RequestHeader, messages: Messages)获取当前 CSRF 令牌当前 CSRF 令牌可通过CSRF.getToken方法获取它接收隐式RequestHeader因此请确保作用域中存在一个val token: Option[CSRF.Token] CSRF.getToken注意安装 CSRF 过滤器后只要所用 cookie 是 HttpOnly 的即 JavaScript 无法访问Play 会尽量避免生成令牌。当响应体是严格strict主体时除非已经调用过CSRF.getTokenPlay 会跳过在响应中添加令牌——这对不需要 CSRF 令牌的响应带来显著性能提升。如果 cookie 未配置为 HttpOnlyPlay 会假定你需要从 JavaScript 访问它而照常生成。如果不使用 CSRF 过滤器还需要注入CSRFAddToken和CSRFCheckAction 包装器强制在特定 Action 上添加令牌或执行检查否则令牌不可用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 提供了一些模板辅助方法。第一个把令牌添加到 Action URL 的查询字符串中参见 csrf.scala.htmlimport helper._ form(CSRF(routes.ItemsController.save())) { ... }可能渲染出的表单形如form methodPOST action/items?csrfToken1234567890abcdef ... /form如果不想让令牌出现在查询字符串中Play 还提供把 CSRF 令牌作为表单隐藏字段的辅助方法form(routes.ItemsController.save()) { CSRF.formField ... }可能渲染出的表单形如form methodPOST action/items input typehidden namecsrfToken value1234567890abcdef/ ... /form从 views/html/helper/CSRF.scala 源码可见这两个辅助方法的实现CSRF(call)通过反向路由Call在 URL 中追加tokenNametokenValueCSRF.formField渲染带转义值的隐藏input字段转义处理防止 XSS。向 Session 添加 CSRF 令牌为确保表单渲染时有令牌可用并能回传给客户端全局过滤器会对所有接受 HTML 的 GET 请求生成新令牌如果传入请求中尚无可用的令牌。源码中对应逻辑位于 CSRFActions.scala当请求头中没有令牌且createIfNotFound返回true时生成新令牌并通过addTokenToResponse写入响应。按 Action 应用 CSRF 过滤有时全局 CSRF 过滤并不合适例如应用可能需要允许某些跨域表单提交。一些不基于会话的标准如 OpenID 2.0要求使用跨站表单提交或在服务器到服务器的 RPC 通信中使用表单提交。此时 Play 提供两个可与应用 Action 组合的 Action 包装器。第一个是CSRFCheckAction负责执行检查应添加到所有接受会话认证 POST 表单提交的 Action 上import play.filters.csrf._ def save checkToken { Action { implicit req // 处理请求体 Ok } }第二个是CSRFAddTokenAction若传入请求尚无令牌则生成一个应添加到所有渲染表单的 Action 上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 invokeBlockA Future[Result]) { // 认证代码 block(request) } override def composeActionA checkToken(action) } class GetAction Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) { override def invokeBlockA Future[Result]) { // 认证代码 block(request) } override def composeActionA addToken(action) }这样就能将样板代码降到最低直接写出简洁的 Actiondef save: Action[AnyContent] postAction { // 处理请求体 Ok } def form: Action[AnyContent] getAction { implicit req Ok(views.html.itemsForm) }在 CSRFActions.scala 中CSRFCheck和CSRFAddToken均实现为可注入的case class分别包装目标 ActionCSRFCheck从请求头/查询字符串/请求体中提取令牌并与会话令牌比对失败则清空令牌并交给错误处理器CSRFAddToken在无令牌时生成新令牌并把令牌写入响应。CSRF 配置选项完整配置项可在过滤器的 reference.conf 中查看配置解析逻辑见 csrf.scala 中的CSRFConfig.fromConfiguration。以下是一些常用选项配置项说明默认值play.filters.csrf.token.namesession 与请求体/查询字符串中使用的令牌名称csrfTokenplay.filters.csrf.cookie.name若配置Play 将 CSRF 令牌存储在以该名称命名的 cookie 中而非 session 中null存于 sessionplay.filters.csrf.cookie.secure设置cookie.name后CSRF cookie 是否设置 secure 标志与play.http.session.secure相同play.filters.csrf.cookie.httpOnlyCSRF cookie 是否设置 HttpOnly 标志注意 CSRF cookie 的默认值与此处会话 cookie 的默认值不同falseplay.filters.csrf.body.bufferSize为从请求体中读取令牌Play 必须先缓冲请求体并可能解析它此配置设置缓冲的最大字节数100k实际为play.http.parser.maxMemoryBufferplay.filters.csrf.token.sign是否使用签名 CSRF 令牌签名令牌确保每次请求的令牌值随机化从而抵御 BREACH 式攻击trueplay.filters.csrf.header.name接受 CSRF 令牌的请求头名称Csrf-Tokenplay.filters.csrf.header.protectHeaders定义必须存在的请求头才执行 CSRF 检查设置为null或空对象则保护所有请求{ Cookie *, Authorization * }play.filters.csrf.header.bypassHeaders定义只要存在即可绕过 CSRF 检查的请求头{}play.filters.csrf.method.whiteList非空时不在该列表中的方法才执行检查[GET, HEAD, OPTIONS]play.filters.csrf.method.blackList仅当白名单为空时使用只检查列表内的方法[]play.filters.csrf.contentType.whiteList/blackList内容类型维度的白/黑名单均空则检查所有内容类型[]play.filters.csrf.routeModifiers.whiteList路由不携带该修饰符时执行检查这是nocsrf修饰符的启用方式[nocsrf]play.filters.csrf.bypassCorsTrustedOrigins是否放行 CORS 过滤器信任来源的请求trueplay.filters.csrf.errorHandler校验失败时的错误处理器ScalaErrorHandler或 JavaCSRFErrorHandler的 FQCN或providednull默认委托给HttpRequestHandler从 csrf.scala 的CSRFConfig定义可以看到这些配置在代码中的对应字段例如postBodyBuffer: Long 102400、signTokens: Boolean true、checkMethod默认排除SafeMethods即GET/HEAD/OPTIONS。令牌签名开关还决定注入哪种TokenProvider启用签名时使用SignedTokenProvider否则使用UnsignedTokenProvider。令牌校验的底层实现在 CSRFActions.scala 中CSRFAction.apply展示了完整的校验流程先从请求头标记令牌tagRequestFromHeader若启用了签名令牌还会执行提取后再签名使令牌每次请求随机化以防御 BREACH见tagRequestFromHeader方法对不安全方法与内容类型的请求先检查是否确实需要 CSRF 检查requiresCsrfCheck若 CORS 已信任来源则放行需要检查时从 session/cookie 取令牌getTokenToValidate再从查询字符串或Csrf-Token头取令牌getHeaderToken两者比对查询字符串中没有令牌时转入请求体检查对application/x-www-form-urlencoded与multipart/form-data分别用extractTokenFromFormBody/extractTokenFromMultipartFormDataBody做轻量解析通过BodyHandler缓冲请求体直到达到body.bufferSize限制后校验校验失败则以NoTokenInBody异常终止流并返回错误响应。使用编译期依赖注入的 CSRF如果应用使用编译期依赖注入以上所有功能都可以使用。装配过程由 CSRFComponents trait 辅助可混入应用组件的 cake 中。关于编译期依赖注入的更多细节请参阅 ScalaCompileTimeDependencyInjection 文档。测试 CSRF渲染页面时可能需要把 CSRF 令牌添加到模板中。可以导入play.api.test.CSRFTokenHelper._它为play.api.test.FakeRequest扩展了withCSRFToken方法参见 UserControllerSpec.scalaimport 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) } } } }CSRFTokenHelper的实现位于 CSRFTokenHelper.scala它通过withCSRFToken为FakeRequest注入有效的 CSRF 令牌使模板渲染测试能够正常通过 CSRF 校验。仓库中的测试还展示了更细粒度的行为验证例如 CSRFFilterSpec.scala 与 CSRFCommonSpecs.scala 覆盖了令牌校验、错误状态码如FORBIDDEN、查询字符串与请求体令牌提取等场景可作为编写自身 CSRF 测试的参考。总结Play Framework 将 CSRF 防护设计为默认保守、按需放行的多层体系全局CSRFFilter自动拦截不安全方法且带会话凭据的请求双令牌比对机制保证表单提交的真实性nocsrf路由修饰符、bypassHeaders、CORS 信任来源等开关提供精确的放行能力CSRFCheck与CSRFAddToken两个 Action 包装器让按 Action 的细粒度控制成为可能签名令牌、HttpOnly cookie 与严格主体的令牌延迟生成策略则在安全与性能之间取得平衡。结合本文给出的配置项与源码路径你可以在自己的 Scala 应用中快速落地一套完整、可验证的 CSRF 防护方案。/DSMLparameter /DSMLinvoke【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考