文档

§配置内容安全策略标头

良好的内容安全策略 (CSP) 是保护网站安全的重要组成部分。正确使用 CSP 可以使 XSS 和注入攻击对攻击者更加困难,尽管一些攻击仍然可能

Play 具有内置功能来处理 CSP,包括对 CSP nonce 和哈希的丰富支持。主要有两种方法:一种是基于过滤器的,它将 CSP 标头添加到所有响应中;另一种是基于操作的,它只在显式包含 CSP 时添加 CSP。

注意SecurityHeaders 过滤器 在配置中有一个 contentSecurityPolicy 属性,该属性已弃用。请参阅弃用部分

§启用 CSPFilter

CSPFilter 默认情况下将在所有请求上设置内容安全策略标头。

§通过配置启用

您可以通过将其添加到 application.conf 中来启用新的 play.filters.csp.CSPFilter

play.filters.enabled += play.filters.csp.CSPFilter

§通过编译时启用

CSP 组件可用作编译时组件,如编译时默认过滤器中所述。

要在 Scala 编译时 DI 中添加过滤器,请包含 play.filters.csp.CSPComponents 特性。

要在 Java 编译时 DI 中添加过滤器,请包含 play.filters.components.CSPComponents

Java
public class MyComponents extends BuiltInComponentsFromContext
    implements HttpFiltersComponents, CSPComponents {

  public MyComponents(ApplicationLoader.Context context) {
    super(context);
  }

  @Override
  public List<play.mvc.EssentialFilter> httpFilters() {
    List<EssentialFilter> parentFilters = HttpFiltersComponents.super.httpFilters();
    List<EssentialFilter> newFilters = new ArrayList<>();
    newFilters.add(cspFilter().asJava());
    newFilters.addAll(parentFilters);
    return newFilters;
  }

  @Override
  public Router router() {
    return Router.empty();
  }
}
Scala
class MyComponents(context: Context)
    extends BuiltInComponentsFromContext(context)
    with HttpFiltersComponents
    with CSPComponents {
  override def httpFilters: Seq[EssentialFilter] = super.httpFilters :+ cspFilter

  lazy val router = Router.empty
}

§使用路由修饰符选择性地禁用过滤器

添加过滤器将向每个请求添加一个 Content-Security-Policy 标头。可能存在您不希望过滤器应用的单个路由,在这种情况下可以使用 nocsp 路由修饰符,使用路由修饰符语法

在您的 conf/routes 文件中

+ nocsp
GET     /my-nocsp-route         controllers.HomeController.myAction

这将从 CSP 过滤器中排除 GET /my-csp-route 路由。

如果您希望为单个路由提供自定义的Content-Security-Policy标头,您可以使用此修饰符将该路由从 CSP 过滤器中排除,然后使用操作的ResultwithHeaders方法来指定自定义的Content-Security-Policy标头。

§在特定操作上启用 CSP

如果在所有路由上启用 CSP 不切实际,则可以改为在特定操作上启用 CSP。

Java
public class CSPActionController extends Controller {
  @CSP
  public Result index() {
    return ok("result with CSP header");
  }
}
Scala
class CSPActionController @Inject() (cspAction: CSPActionBuilder, cc: ControllerComponents)
    extends AbstractController(cc) {
  def index: Action[AnyContent] = cspAction { implicit request => Ok("result containing CSP") }
}

§配置 CSP

CSP 过滤器主要通过play.filters.csp部分下的配置驱动。

§SecurityHeaders.contentSecurityPolicy 的弃用

SecurityHeaders 过滤器在配置中有一个contentSecurityPolicy属性,该属性已弃用。该功能仍然启用,但contentSecurityPolicy属性的默认设置已从default-src 'self'更改为null

如果play.filters.headers.contentSecurityPolicy不为空,您将收到警告。从技术上讲,可以同时启用contentSecurityPolicy和新的CSPFilter,但不建议这样做。

注意:您需要仔细查看 CSP 过滤器中指定的 Content Security Policy,以确保它满足您的需求,因为它与之前的contentSecurityPolicy有很大不同。

§配置 CSP 报告

当在conf/application.conf中配置 CSP report-toreport-uri CSP 指令时,违反这些指令的页面将向给定 URL 发送报告。

play.filters.csp {
  directives {
    report-to = "http://localhost:9000/report-to"
    report-uri = ${play.filters.csp.directives.report-to}
  }
}

CSP 报告格式为 JSON。为了方便起见,Play 提供了一个主体解析器,可以解析 CSP 报告,这在首次采用 CSP 策略时非常有用。您可以添加一个 CSP 报告控制器,以便在您方便的时候发送或存储 CSP 报告。

Java
public class CSPReportController extends Controller {

  private final Logger logger = LoggerFactory.getLogger(getClass());

  @BodyParser.Of(CSPReportBodyParser.class)
  public Result cspReport(Http.Request request) {
    JavaCSPReport cspReport = request.body().as(JavaCSPReport.class);
    logger.warn(
        "CSP violation: violatedDirective = {}, blockedUri = {}, originalPolicy = {}",
        cspReport.violatedDirective(),
        cspReport.blockedUri(),
        cspReport.originalPolicy());

    return Results.ok();
  }
}
Scala
class CSPReportController @Inject() (cc: ControllerComponents, cspReportAction: CSPReportActionBuilder)
    extends AbstractController(cc) {
  private val logger = org.slf4j.LoggerFactory.getLogger(getClass)

  val report: Action[ScalaCSPReport] = cspReportAction { request =>
    val report = request.body
    logger.warn(
      s"CSP violation: violated-directive = ${report.violatedDirective}, " +
        s"blocked = ${report.blockedUri}, " +
        s"policy = ${report.originalPolicy}"
    )
    Ok("{}").as(JSON)
  }
}

要配置控制器,请将其作为路由添加到conf/routes中。

+ nocsrf
POST     /report-to                 controllers.CSPReportController.report

请注意,如果您启用了 CSRF 过滤器,您可能需要+ nocsrf路由修饰符,或者将play.filters.csrf.contentType.whiteList += "application/csp-report"添加到application.conf中以将 CSP 报告列入白名单。

§仅配置 CSP 报告

CSP 还具有“仅报告”功能,该功能会导致浏览器允许页面呈现,同时仍然将 CSP 报告发送到给定 URL。

除了在conf/application.conf中配置report-toreport-uri CSP 指令外,通过设置reportOnly标志可以启用报告功能。

play.filters.csp.reportOnly = true

CSP 报告有四种不同的样式:“Blink”、“Firefox”、“Webkit”和“Old Webkit”。Zack Tollman 有一篇很好的博客文章What to Expect When Expecting Content Security Policy Reports,详细讨论了每种样式。

§配置 CSP 哈希

CSP 允许通过 对内容进行哈希 并将其作为指令提供来将内联脚本和样式列入白名单。

Play 提供了一组配置的哈希值,可用于通过引用模式来组织哈希值。在 application.conf

play.filters.csp {
  hashes += {
    algorithm = "sha256"
    hash = "RpniQm4B6bHP0cNtv7w1p6pVcgpm5B/eu1DNEYyMFXc="
    pattern = "%CSP_MYSCRIPT_HASH%"
  }
  style-src = "%CSP_MYSCRIPT_HASH%"
}

哈希值可以通过 在线哈希计算器 计算,也可以使用实用程序类在内部生成。

Java
public class CSPHashGenerator {

  private final String digestAlgorithm;
  private final MessageDigest digestInstance;

  public CSPHashGenerator(String digestAlgorithm) throws NoSuchAlgorithmException {
    this.digestAlgorithm = digestAlgorithm;
    switch (digestAlgorithm) {
      case "sha256":
        this.digestInstance = MessageDigest.getInstance("SHA-256");
        break;
      case "sha384":
        this.digestInstance = MessageDigest.getInstance("SHA-384");
        break;
      case "sha512":
        this.digestInstance = MessageDigest.getInstance("SHA-512");
        break;
      default:
        throw new IllegalArgumentException("Unknown digest " + digestAlgorithm);
    }
  }

  public String generateUTF8(String str) {
    return generate(str, StandardCharsets.UTF_8);
  }

  public String generate(String str, Charset charset) {
    byte[] bytes = str.getBytes(charset);
    return encode(digestInstance.digest(bytes));
  }

  private String encode(byte[] digestBytes) {
    String rawHash = Base64.getMimeEncoder().encodeToString(digestBytes);
    return String.format("'%s-%s'", digestAlgorithm, rawHash);
  }
}
Scala
class CSPHashGenerator(digestAlgorithm: String) {
  private val digestInstance: MessageDigest = {
    digestAlgorithm match {
      case "sha256" =>
        MessageDigest.getInstance("SHA-256")
      case "sha384" =>
        MessageDigest.getInstance("SHA-384")
      case "sha512" =>
        MessageDigest.getInstance("SHA-512")
    }
  }

  def generateUTF8(str: String): String = {
    generate(str, StandardCharsets.UTF_8)
  }

  def generate(str: String, charset: Charset): String = {
    val bytes = str.getBytes(charset)
    encode(digestInstance.digest(bytes))
  }

  protected def encode(digestBytes: Array[Byte]): String = {
    val rawHash = Base64.getMimeEncoder.encodeToString(digestBytes)
    s"'$digestAlgorithm-$rawHash'"
  }
}

§配置 CSP Nonce

CSP nonce 是一个“一次性”值 (n=once),它在每次请求时生成,可以插入内联内容的主体中以将内容列入白名单。

如果 play.filters.csp.nonce.enabled 为 true,则 Play 通过 play.filters.csp.DefaultCSPProcessor 定义 nonce。如果请求具有属性 play.api.mvc.request.RequestAttrKey.CSPNonce,则使用该 nonce。否则,将从 16 字节的 java.security.SecureRandom 生成 nonce。

# Specify a nonce to be used in CSP security header
# https://www.w3.org/TR/CSP3/#security-nonces
#
# Nonces are used in script and style elements to protect against XSS attacks.
nonce {
  # Use nonce value (generated and passed in through request attribute)
  enabled = true

  # Pattern to use to replace with nonce
  pattern = "%CSP_NONCE_PATTERN%"

  # Add the nonce to "X-Content-Security-Policy-Nonce" header.  This is useful for debugging.
  header = false
}

从 Twirl 模板访问 CSP nonce 如 在页面模板中使用 CSP 中所示。

§配置 CSP 指令

CSP 指令通过 application.conf 中的 play.filters.csp.directives 部分进行配置。

§定义 CSP 指令

指令是一对一配置的,配置键与 CSP 指令名称匹配,例如,对于 CSP 指令 default-src,其值为 'none',您将设置以下内容

play.filters.csp.directives.default-src = "'none'"

如果未指定值,则应使用 "",例如,upgrade-insecure-requests 将定义如下

play.filters.csp.directives.upgrade-insecure-requests = ""

CSP 指令主要在 CSP3 规范 中定义,但以下情况除外

CSP 备忘单 是查找 CSP 指令的良好参考。

§默认 CSP 策略

CSPFilter 中定义的默认策略基于 Google 的 严格 CSP 策略

# The directives here are set to the Google Strict CSP policy by default
# https://csp.withgoogle.com/docs/strict-csp.html
directives {
  # base-uri defaults to 'none' according to https://csp.withgoogle.com/docs/strict-csp.html
  # https://www.w3.org/TR/CSP3/#directive-base-uri
  base-uri = "'none'"

  # object-src defaults to 'none' according to https://csp.withgoogle.com/docs/strict-csp.html
  # https://www.w3.org/TR/CSP3/#directive-object-src
  object-src = "'none'"

  # script-src defaults according to https://csp.withgoogle.com/docs/strict-csp.html
  # https://www.w3.org/TR/CSP3/#directive-script-src
  script-src = ${play.filters.csp.nonce.pattern} "'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:"
}

注意: Google 的严格 CSP 策略是一个良好的起点,但它并没有完全定义内容安全策略。请咨询安全团队以确定适合您网站的策略。

§在页面模板中使用 CSP

可以使用 views.html.helper.CSPNonce 辅助类从页面模板访问 CSP nonce。此辅助类具有多种方法,可以以不同的方式呈现 nonce。

§CSPNonce 辅助类

注意: 您必须在所有上述方法的范围内有一个隐式的 RequestHeader,例如 @()(implicit request: RequestHeader)

§将 CSPNonce 添加到 HTML

将 CSP nonce 添加到页面模板最简单的方法是在 HTML 元素中添加 @{CSPNonce.attr}

例如,要将 CSP nonce 添加到 link 元素,您需要执行以下操作

@()(implicit request: RequestHeader)

<link rel="stylesheet" @{CSPNonce.attr}  media="screen" href="@routes.Assets.at("stylesheets/main.css")">

在现有辅助类接受属性映射的情况下,使用 CSPNonce.attrMap 是合适的。例如,WebJars 项目将接受属性

@()(implicit request: RequestHeader, webJarsUtil: org.webjars.play.WebJarsUtil)

@webJarsUtil.locate("bootstrap.min.css").css(CSPNonce.attrMap)
@webJarsUtil.locate("bootstrap-theme.min.css").css(CSPNonce.attrMap)

@webJarsUtil.locate("jquery.min.js").script(CSPNonce.attrMap)

§支持 CSPNonce 的辅助类

为了方便使用,有 stylescript 辅助类,它们将包装现有的内联块。这些对于添加简单的内联 Javascript 和 CSS 代码非常有用。

由于这些助手是通过 Twirl 模板生成的,因此 Scaladoc 不会提供这些助手的正确源代码引用。这些助手的源代码可以在 Github 上查看,以获得更完整的视图。

§样式助手

style 助手是以下内容的包装器

<style @{CSPNonce.attr} @toHtmlArgs(args.toMap)>@body</style>

它在页面中使用,例如

@()(implicit request: RequestHeader)

@views.html.helper.style(Symbol("type") -> "text/css") {
    html, body, pre {
        margin: 0;
        padding: 0;
        font-family: Monaco, 'Lucida Console', monospace;
        background: #ECECEC;
    }
}
§脚本助手

script 助手是脚本元素的包装器

<script @{CSPNonce.attr} @toHtmlArgs(args.toMap)>@body</script>

它按如下方式使用

@()(implicit request: RequestHeader)

@views.html.helper.script(args = Symbol("type") -> "text/javascript") {
  alert("hello world");
}

§动态启用 CSP

在上面给出的示例中,CSP 是从配置中处理的,并且是静态完成的。如果您需要在运行时更改 CSP 策略,或者拥有多个不同的策略,那么创建和动态添加 CSP 标头可能比使用操作或过滤器更有意义,并将该标头与 CSP 的配置过滤器结合使用。

§使用 CSPProcessor

假设您有许多资产,并且想要动态地将 CSP 哈希添加到您的标头中。以下是如何使用自定义操作构建器注入动态 CSP 哈希列表

§Scala

package controllers {
  import javax.inject._

  import scala.concurrent.ExecutionContext

  import org.apache.pekko.stream.Materializer
  import play.api.mvc._
  import play.filters.csp._

  // Custom CSP action
  class AssetAwareCSPActionBuilder @Inject() (
      bodyParsers: PlayBodyParsers,
      cspConfig: CSPConfig,
      assetCache: AssetCache
  )(
      implicit protected override val executionContext: ExecutionContext,
      protected override val mat: Materializer
  ) extends CSPActionBuilder {
    override def parser: BodyParser[AnyContent] = bodyParsers.default

    // processor with dynamically generated config
    protected override def cspResultProcessor: CSPResultProcessor = {
      val modifiedDirectives: Seq[CSPDirective] = cspConfig.directives.map {
        case CSPDirective(name, value) if name == "script-src" =>
          CSPDirective(name, value + assetCache.cspDigests.mkString(" "))
        case csp: CSPDirective =>
          csp
      }

      CSPResultProcessor(CSPProcessor(cspConfig.copy(directives = modifiedDirectives)))
    }
  }

  // Dummy class that can have a dynamically changing list of csp-hashes
  class AssetCache {
    def cspDigests: Seq[String] = {
      Seq(
        "sha256-HELLO",
        "sha256-WORLD"
      )
    }
  }

  class HomeController @Inject() (cc: ControllerComponents, myCSPAction: AssetAwareCSPActionBuilder)
      extends AbstractController(cc) {
    def index = myCSPAction {
      Ok("I have an asset aware header!")
    }
  }
}

import com.google.inject.AbstractModule

class CSPModule extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[controllers.AssetCache]).asEagerSingleton()
    bind(classOf[controllers.AssetAwareCSPActionBuilder]).asEagerSingleton()
  }
}

§Java

相同的原理适用于 Java,只是扩展了 AbstractCSPAction

public class MyDynamicCSPAction extends AbstractCSPAction {

  private final AssetCache assetCache;
  private final CSPConfig cspConfig;

  @Inject
  public MyDynamicCSPAction(CSPConfig cspConfig, AssetCache assetCache) {
    this.assetCache = assetCache;
    this.cspConfig = cspConfig;
  }

  private CSPConfig cspConfig() {
    return cspConfig.withDirectives(generateDirectives());
  }

  private List<CSPDirective> generateDirectives() {
    List<CSPDirective> baseDirectives = CollectionConverters.asJava(cspConfig.directives());
    return baseDirectives.stream()
        .map(
            directive -> {
              if ("script-src".equals(directive.name())) {
                String scriptSrc = directive.value();
                String newScriptSrc = scriptSrc + " " + String.join(" ", assetCache.cspHashes());
                return new CSPDirective("script-src", newScriptSrc);
              } else {
                return directive;
              }
            })
        .collect(Collectors.toList());
  }

  @Override
  public CSPProcessor processor() {
    return new DefaultCSPProcessor(cspConfig());
  }
}
public class AssetCache {
  public List<String> cspHashes() {
    return Collections.singletonList("sha256-HELLO");
  }
}
public class CustomCSPActionModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(MyDynamicCSPAction.class).asEagerSingleton();
    bind(AssetCache.class).asEagerSingleton();
  }
}

然后在您的操作上调用 @With(MyDynamicCSPAction.class)

§CSP 注意事项

CSP 是一款强大的工具,但它也结合了许多不同的指令,这些指令并不总是能顺利地协同工作。

§不直观的指令

某些指令不受 default-src 的覆盖,例如 form-action 是单独定义的。在 我正在从您的网站上获取信用卡号码和密码。方法如下 中详细介绍了针对省略 form-action 的网站的攻击。

特别是,CSP 有许多微妙的交互作用,这些交互作用并不直观。例如,如果您使用的是 WebSockets,则应使用确切的 URL(即 ws://localhost:9000 wss://localhost:9443)启用 connect-src,因为 使用 connect-src 'self' 声明 CSP 不会允许 WebSockets 返回到同一主机/端口,因为它们不是同源的。如果您没有设置 connect-src,那么您应该检查 Origin 标头以防止 跨站点 WebSocket 劫持

§错误的 CSP 报告

浏览器扩展和插件 中可能会产生许多误报,这些误报可能显示为来自 about:blank。解决真实问题并制定过滤器可能需要很长时间。如果您希望在外部配置仅报告策略,Report URI 是一项托管的 CSP 服务,它将收集 CSP 报告并提供过滤器。

§进一步阅读

采用良好的 CSP 策略是一个多阶段的过程。Google 的 采用严格的 CSP 指南值得推荐,但它仅仅是一个起点,CSP 的实现还有一些非平凡的方面。

Github 关于 实现 CSP 和添加 额外保护 的讨论值得一读。

Dropbox 发布了关于 CSP 报告和过滤 以及 内联内容和 nonce 部署 的文章,并在切换到强制执行的 CSP 策略之前经历了长时间的 CSP 报告阶段。

Square 也撰写了关于 单页 Web 应用程序的内容安全策略 的文章。

下一步:配置允许的主机


发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读了 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区进行交流。