Documentation

You are viewing the documentation for the 2.8.x release series. The latest stable release series is 3.0.x.

§Configuring Content Security Policy Headers

A good content security policy (CSP) is an essential part of securing a website. Used properly, CSP can make XSS and injection much harder for attackers, although some attacks are still possible.

Play has a built in functionality for working with CSP, including rich support for CSP nonces and hashes. There are two main approaches: a filter based approach that adds a CSP header to all responses, and an action based approach that only adds CSP when explicitly included.

Note: The SecurityHeaders filter has a contentSecurityPolicy property in configuration is deprecated. Please see the deprecation section.

§Enabling CSPFilter

The CSPFilter will set the content security policy header on all requests by default.

§Enabling Through Configuration

You can enable the new play.filters.csp.CSPFilter by adding it to application.conf:

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

§Enabling Through Compile Time

The CSP components are available as compile time components, as described in Compile Time Default Filters.

To add the filter in Scala compile time DI, include the play.filters.csp.CSPComponents trait.

To add the filter in Java compile time DI, include the 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
}

§Selectively Disabling Filter with Route Modifier

Adding the filter will add a Content-Security-Policy header to every request. There may be individual routes where you do not want the filter to apply, and the nocsp route modifier may be used here, using the route modifier syntax.

In your conf/routes file:

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

This excludes the GET /my-csp-route route from the CSP filter.

If you wish to provide a custom Content-Security-Policy header for only a single route you can exclude the route from the CSP filter with this modifier and then use the withHeaders method of your action’s Result to specify a custom Content-Security-Policy header.

§Enabling CSP on Specific Actions

If enabling CSP across all routes is not practical, CSP can be enabled on specific actions instead:

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 = cspAction { implicit request =>
    Ok("result containing CSP")
  }
}

§Configuring CSP

The CSP filter is driven primarily through configuration under the play.filters.csp section.

§Deprecation of SecurityHeaders.contentSecurityPolicy

The SecurityHeaders filter has a contentSecurityPolicy property in configuration is deprecated. The functionality is still enabled, but contentSecurityPolicy property’s default setting has been changed from default-src ‘self’ to null.

If play.filters.headers.contentSecurityPolicy is not null, you will receive a warning. It is technically possible to have contentSecurityPolicy and the new CSPFilter active at the same time, but this is not recommended.

Note: You will want to review the Content Security Policy specified in the CSP filter closely to ensure it meets your needs, as it differs substantially from the previous contentSecurityPolicy.

§Configuring CSP Reports

When the CSP report-to or report-uri CSP directives in conf/application.conf are configured, a page that violates the directives will send a report to the given URL.

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

CSP reports are formatted as JSON. For your convenience, Play provides a body parser that can parse a CSP report, useful when first adopting a CSP policy. You can add a CSP report controller to send or store the CSP report at your convenience:

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

To configure the controller, add it as a route in conf/routes:

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

Note that if you have the CSRF filter enabled, you may need + nocsrf route modifier, or add play.filters.csrf.contentType.whiteList += "application/csp-report" to application.conf to whitelist CSP reports.

§Configuring CSP Report Only

CSP also has a “report only” feature which results in the browser allowing the page to render, while still sending a CSP report to a given URL.

The report feature is enabled by setting the reportOnly flag in addition to configuring the report-to and report-uri CSP directives in conf/application.conf:

play.filters.csp.reportOnly = true

CSP reports come in four different styles: “Blink”, “Firefox”, “Webkit”, and “Old Webkit”. Zack Tollman has a good blog post What to Expect When Expecting Content Security Policy Reports that discusses each style in detail.

§Configuring CSP Hashes

CSP allows inline scripts and styles to be whitelisted by hashing the contents and providing it as a directive.

Play provides an array of configured hashes that can be used to organize hashes via a referenced pattern. In application.conf:

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

Hashes can be calculated through an online hash calculator, or generated internally using a utility class:

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'"
  }
}

§Configuring CSP Nonces

A CSP nonce is a “one time only” value (n=once) that is generated on every request and can be inserted into the body of inline content to whitelist content.

Play defines a nonce through play.filters.csp.DefaultCSPProcessor if play.filters.csp.nonce.enabled is true. If a request has the attribute play.api.mvc.request.RequestAttrKey.CSPNonce, then that nonce is used. Otherwise, a nonce is generated from 16 bytes of 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
}

Accessing the CSP nonce from a Twirl template is shown in Using CSP in Page Templates.

§Configuring CSP Directives

CSP directives are configured through the play.filters.csp.directives section in application.conf.

§Defining CSP Directives

Directives are configured one to one, with the configuration key matching the CSP directive name, i.e. for a CSP directive default-src with a value of 'none', you would set the following:

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

Where no value is specified then "" should be used, i.e. upgrade-insecure-requests would be defined as follows:

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

CSP directives are mostly defined in the CSP3 Spec except for the following exceptions:

The CSP cheat sheet is a good reference for looking up CSP directives.

§Default CSP Policy

The default policy defined in CSPFilter is based off Google’s Strict CSP Policy:

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

Note: Google’s Strict CSP policy is a good place to start, but it does not completely define a content security policy. Please consult with a security team to determine the right policy for your site.

§Using CSP in Page Templates

The CSP nonce is accessible from page templates using the views.html.helper.CSPNonce helper class. This helper has a number of methods that render the nonce in different ways.

§CSPNonce Helper

Note: You must have an implicit RequestHeader in scope for all the above methods, i.e. @()(implicit request: RequestHeader)

§Adding CSPNonce to HTML

Adding the CSP nonce into page templates is most easily done by adding @{CSPNonce.attr} into an HTML element.

For example, to add a CSP nonce to a link element, you would do the following:

@()(implicit request: RequestHeader)

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

Using CSPNonce.attrMap is appropriate in cases where existing helpers take a map of attributes. For example, the WebJars project will take attributes:

@()(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 aware helpers

For ease of use, there are style, and script helpers that will wrap existing inline blocks. These are useful for adding simple bits of inline Javascript and CSS.

Because these helpers are generated from Twirl templates, Scaladoc does not provide the correct source reference for these helpers. The source code for these helpers can be seen on Github for a more complete view.

§Style Helper

The style helper is a wrapper for the following:

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

And is used in pages like this:

@()(implicit request: RequestHeader)

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

The script helper is a wrapper for the script element:

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

and is used as follows:

@()(implicit request: RequestHeader)

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

§Enabling CSP Dynamically

In the examples given above, CSP is processed from configuration, and is done so statically. If you need to change your CSP policy at runtime, or have several different policies, then it may make more sense to create and add a CSP header dynamically rather than use an action or a filter, and combine that with CSP’s configured filter.

§Using CSPProcessor

Say that you have a number of assets, and you want to add CSP hashes to your header dynamically. Here’s how you would inject a dynamic list of CSP hashes using a custom action builder:

§Scala

package controllers {
  import akka.stream.Materializer
  import javax.inject._
  import play.api.mvc._
  import play.filters.csp._

  import scala.concurrent.ExecutionContext

  // 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

The same principal applies in Java, only extending the 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() {
    // import scala.collection.JavaConverters;
    List<CSPDirective> baseDirectives = JavaConverters.seqAsJavaList(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();
  }
}

And then call @With(MyDynamicCSPAction.class) on your action.

§CSP Gotchas

CSP is a powerful tool, but it also combines a number of disparate directives that do not always work together smoothly.

§Unintuitive Directives

Some directives are not covered by default-src, for example form-action is defined separately. An attack on a website omitting form-action was detailed in I’m harvesting credit card numbers and passwords from your site. Here’s how.

In particular, there are a number of subtle interactions with CSP that are unintuitive. For example, if you are using websockets, you should enable the connect-src with the exact URL (i.e. ws://localhost:9000 wss://localhost:9443) as declaring a CSP with connect-src ‘self’ will not allow websockets back to the same host/port, since they’re not same origin. If you do not set connect-src, then you should check the Origin header to protect against Cross-Site WebSocket Hijacking.

§False CSP Reports

There can be a number of false positives produced from browser extensions and plugins, and these can show as coming from about:blank. It can take an extended period of time to resolve real issues and work out filters. If you would rather configure a report-only policy externally, Report URI is a hosted CSP service that will collect CSP reports and provide filters.

§Further Reading

Adopting a good CSP policy is a multi-stage process. Google’s Adopting strict CSP guide is recommended, but is only a starting point, and there are some non-trivial aspects to CSP implementation.

Github’s discussion on implementing CSP and adding additional protections is worth reading.

Dropbox has posts on CSP reporting and filtering and inline content and nonce deployment, and went through extended periods of reporting CSP before moving to an enforced CSP policy.

Square also wrote up Content Security Policy for Single Page Web Apps.

Next: Configuring allowed hosts


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request. Have questions or advice to share? Go to our community forums to start a conversation with the community.