ドキュメント

§コンテンツセキュリティポリシーヘッダーの設定

適切なコンテンツセキュリティポリシー (CSP) は、ウェブサイトのセキュリティ確保に不可欠です。正しく使用すれば、CSP は攻撃者にとって XSS やインジェクションをはるかに困難にしますが、一部の攻撃は 依然として可能です

Play は、CSP nonce とハッシュの豊富なサポートを含む、CSP を操作するための組み込み機能を備えています。主なアプローチは 2 つあります。すべてのレスポンスに CSP ヘッダーを追加するフィルターベースのアプローチと、明示的に含まれている場合にのみ CSP を追加するアクションベースのアプローチです。

注記: SecurityHeaders フィルター の設定には、contentSecurityPolicy プロパティがありますが、非推奨です。非推奨に関するセクション を参照してください。

§CSPFilter の有効化

CSPFilter は、デフォルトですべてのリクエストにコンテンツセキュリティポリシーヘッダーを設定します。

§設定による有効化

新しい play.filters.csp.CSPFilterapplication.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 を同時にアクティブにすることは可能ですが、お勧めしません。

注記: 前の 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.confplay.filters.csrf.contentType.whiteList += "application/csp-report" を追加して CSP レポートをホワイトリストに登録する必要がある場合があります。

§CSP レポートのみの設定

CSP には「レポートのみ」機能もあり、これによりブラウザーはページのレンダリングを許可しますが、指定された URL に CSP レポートを送信します。

レポート機能は、conf/application.conf で CSP の report-toreport-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.confplay.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 仕様 で主に定義されています。

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ヘルパー

注記: 上記のすべてのメソッドでは、暗黙的な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対応ヘルパー

使いやすさのために、既存のインラインブロックをラップするstylescriptヘルパーがあります。これらは、シンプルなインライン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アプリのコンテンツセキュリティポリシーについて記事を書いています。

次へ: 許可されたホストの設定


このドキュメントにエラーを発見しましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。