§コンテンツセキュリティポリシーヘッダーの設定
適切なコンテンツセキュリティポリシー (CSP) は、ウェブサイトのセキュリティ確保に不可欠です。正しく使用すれば、CSP は攻撃者にとって XSS やインジェクションをはるかに困難にしますが、一部の攻撃は 依然として可能です。
Play は、CSP nonce とハッシュの豊富なサポートを含む、CSP を操作するための組み込み機能を備えています。主なアプローチは 2 つあります。すべてのレスポンスに 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
を同時にアクティブにすることは可能ですが、お勧めしません。
注記: 前の
contentSecurityPolicy
とは大きく異なるため、CSP フィルターで指定されたコンテンツセキュリティポリシーをよく確認して、ニーズを満たしていることを確認する必要があります。
§CSP レポートの設定
conf/application.conf
で CSP の report-to
または report-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
ルート修飾子が必要になる場合や、application.conf
に play.filters.csrf.contentType.whiteList += "application/csp-report"
を追加して CSP レポートをホワイトリストに登録する必要がある場合があります。
§CSP レポートのみの設定
CSP には「レポートのみ」機能もあり、これによりブラウザーはページのレンダリングを許可しますが、指定された URL に CSP レポートを送信します。
レポート機能は、conf/application.conf
で CSP の report-to
と report-uri
ディレクティブを設定することに加えて、reportOnly
フラグを設定することで有効になります。
play.filters.csp.reportOnly = true
CSP レポートには、「Blink」、「Firefox」、「Webkit」、「Old Webkit」の 4 つの異なるスタイルがあります。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 ディレクティブの定義
ディレクティブは 1 対 1 で設定され、設定キーは 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
については、Upgrade Insecure Requests W3C CRを参照してください。block-all-mixed-content
については、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の使用
CSP nonceは、views.html.helper.CSPNonce
ヘルパークラスを使用してページテンプレートからアクセスできます。このヘルパーには、nonceをさまざまな方法でレンダリングする複数のメソッドがあります。
§CSPNonceヘルパー
CSPNonce.apply
は、nonceを文字列として返します。例外が発生することもあります。CSPNonce.attr
は、nonce="$nonce"
をTwirlのHtml
またはHtml.empty
として返します。CSPNonce.attrMap
は、Map("nonce" -> nonce)
またはMap.empty
を返します。CSPNonce.get
は、Some(nonce)
またはNone
を返します。
注記: 上記のすべてのメソッドでは、暗黙的な
RequestHeader
をスコープ内に含める必要があります(例:@()(implicit request: RequestHeader)
)。
§HTMLへのCSPNonceの追加
ページテンプレートにCSP nonceを追加する最も簡単な方法は、HTML要素に@{CSPNonce.attr}
を追加することです。
たとえば、link
要素にCSP nonceを追加するには、次のようにします。
@()(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
ヘルパーは、次のラッパーです。
<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
ヘルパーは、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との間には、直感に反する微妙な相互作用がいくつかあります。たとえば、WebSocketを使用している場合は、`connect-src 'self'` を宣言しても同じホスト/ポートへのWebSocketは許可されません(同じオリジンではないため)ため、正確なURL(例:ws://localhost:9000 wss://localhost:9443
)を使用してconnect-src
を有効にする必要があります。connect-src
を設定しない場合は、クロスサイトWebSocketハイジャックから保護するために、Origin
ヘッダーを確認する必要があります。
§誤ったCSPレポート
ブラウザの拡張機能とプラグインによって多くの誤検知が発生することがあり、これらはabout:blank
からのものとして表示される可能性があります。実際の問題を解決し、フィルターを作成するには、長い時間がかかる場合があります。外部でレポート専用のポリシーを設定する方が良い場合は、Report URIは、CSPレポートを収集し、フィルターを提供するホスト型のCSPサービスです。
§参考資料
適切なCSPポリシーを採用することは、複数段階のプロセスです。Googleの厳格なCSPの採用ガイドは推奨されますが、これは出発点にすぎず、CSPの実装にはいくつかの非自明な側面があります。
CSPの実装と追加の保護の追加に関するGithubの議論はこちらを読む価値があります。
Dropboxには、CSPレポートとフィルタリングとインラインコンテンツとnonceの展開に関する投稿があり、強制的なCSPポリシーに移行する前に、長期間CSPレポートを行っていました。
SquareもシングルページWebアプリのコンテンツセキュリティポリシーについて記事を書いています。
次へ: 許可されたホストの設定
このドキュメントにエラーを発見しましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。