§設定內容安全性政策標頭
良好的內容安全性政策 (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 篩選器之外,然後使用動作的 Result
的 withHeaders
方法來指定自訂 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-to
或 report-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-to
和 report-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 規範 中,但有下列例外
require-sri-for
說明於 子資源完整性upgrade-insecure-requests
說明於 升級不安全要求 W3C CRblock-all-mixed-content
說明於 混合內容 W3C CR
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 輔助類別
CSPNonce.apply
會傳回隨機數字串或擲回例外。CSPNonce.attr
會傳回nonce="$nonce"
作為 TwirlHtml
或Html.empty
CSPNonce.attrMap
傳回Map("nonce" -> nonce)
或Map.empty
CSPNonce.get
傳回Some(nonce)
或None
注意:您必須在範圍內有一個隱含的
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 的輔助程式
為了易於使用,有 style
和 script
輔助程式,它們會封裝現有的內嵌區塊。這些對於加入簡單的內嵌 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。有問題或建議要分享嗎?前往 我們的社群論壇 與社群展開對話。