文件

§設定內容安全性政策標頭

良好的內容安全性政策 (CSP) 是確保網站安全的重要部分。正確使用時,CSP 可以讓攻擊者更難執行 XSS 和注入,儘管有些攻擊仍然可能

Play 內建與 CSP 合作的功能,包括對 CSP 隨機數和雜湊的豐富支援。有兩種主要方法:一種是基於篩選器的方法,會將 CSP 標頭新增至所有回應,另一種是基於動作的方法,只會在明確包含時新增 CSP。

注意SecurityHeaders 篩選器 的設定檔中有一個 contentSecurityPolicy 屬性已棄用。請參閱 棄用區段

§啟用 CSPFilter

CSPFilter 預設會在所有要求中設定內容安全政策標頭。

§透過設定檔啟用

您可以透過將新的 play.filters.csp.CSPFilter 新增至 application.conf 來啟用它

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

這會將 GET /my-csp-route 路由排除在 CSP 篩選器之外。

如果您希望僅針對單一路由提供自訂 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 不為 null,您將會收到警告。技術上來說,同時啟用 contentSecurityPolicy 和新的 CSPFilter 是可行的,但並不建議這麼做。

注意:您會希望仔細檢閱 CSP 篩選器中指定的內容安全政策,以確保它符合您的需求,因為它與之前的 contentSecurityPolicy 有很大的不同。

§設定 CSP 報告

conf/application.conf 中的 CSP report-toreport-uri CSP 指令設定好後,違反指令的頁面會將報告傳送至指定的 URL。

play.filters.csp {
  directives {
    report-to = "https://127.0.0.1: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 路由修改器,或在 application.conf 中新增 play.filters.csrf.contentType.whiteList += "application/csp-report" 以將 CSP 報告加入白名單。

§設定 CSP 僅報告

CSP 也有「僅報告」功能,讓瀏覽器允許頁面呈現,同時仍將 CSP 報告傳送至指定的 URL。

報告功能可透過設定 reportOnly 旗標,以及在 conf/application.conf 中設定 report-toreport-uri CSP 指令來啟用。

play.filters.csp.reportOnly = true

CSP 報告有四種不同的樣式:「Blink」、「Firefox」、「Webkit」和「舊版 Webkit」。Zack Tollman 有一篇很棒的部落格文章 期待內容安全政策報告時會發生什麼事,詳細討論了每種樣式。

§設定 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 隨機數

CSP 隨機數是「一次性」值 (n=once),在每個要求中產生,並可插入內嵌內容的主體中,以將內容加入白名單。

如果 play.filters.csp.nonce.enabled 為 true,Play 會透過 play.filters.csp.DefaultCSPProcessor 定義隨機數。如果要求有屬性 play.api.mvc.request.RequestAttrKey.CSPNonce,則會使用該隨機數。否則,會從 16 位元組的 java.security.SecureRandom 產生隨機數。

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

使用 CSP 在頁面範本中 中,顯示了如何從 Twirl 範本存取 CSP 隨機數。

§設定 CSP 指令

CSP 指令透過 application.conf 中的 play.filters.csp.directives 區段設定。

§定義 CSP 指令

指令是一對一配置的,其中配置金鑰與 CSP 指令名稱相符,亦即對於值為 'none' 的 CSP 指令 default-src,您會設定下列內容

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 隨機數。此輔助類別有許多方法,可使用不同的方式呈現隨機數。

§CSPNonce 輔助類別

注意:您必須在範圍內有一個隱含的 RequestHeader,例如 @()(implicit request: RequestHeader)

§將 CSPNonce 加入 HTML

將 CSP nonce 加入頁面範本最簡單的方法是將 @{CSPNonce.attr} 加入 HTML 元素。

例如,若要將 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 hash 新增到您的標頭。以下是使用自訂動作產生器注入 CSP hash 動態清單的方法

§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 的微妙互動是不直觀的。例如,如果您正在使用 Websocket,您應該使用確切的 URL(即 ws://127.0.0.1:9000 wss://127.0.0.1:9443)啟用 connect-src,因為 宣告具有 connect-src ‘self’ 的 CSP 將不允許 Websocket 回到同一個主機/埠,因為它們不是同一個來源。如果您沒有設定 connect-src,那麼您應該檢查 Origin 標頭以防範 跨網站 Websocket 劫持

§錯誤的 CSP 報告

可能會產生許多來自 瀏覽器擴充功能和外掛程式 的誤報,這些誤報可能顯示來自 about:blank。解決實際問題和找出過濾器可能需要一段時間。如果您希望在外部設定僅報告政策,報告 URI 是一個託管的 CSP 服務,它將收集 CSP 報告並提供過濾器。

§進一步閱讀

採用良好的 CSP 政策是一個多階段的過程。建議採用 Google 的 採用嚴格 CSP 指南,但這只是一個起點,而且 CSP 實作有一些非平凡的面向。

Github 關於 實作 CSP 和增加 額外防護 的討論值得一讀。

Dropbox 有關於 CSP 報告和過濾內嵌內容和 nonce 部署 的文章,而且在轉移到強制 CSP 政策之前經歷了長時間的 CSP 報告。

Square 也寫了 單頁式網頁應用程式的內容安全政策

下一步:設定允許的主機


在此文件發現錯誤?此頁面的原始碼可在此處找到 here。在閱讀 文件指南 後,請隨時貢獻一個 pull request。有問題或建議要分享嗎?前往 我們的社群論壇 與社群展開對話。