§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: Action[AnyContent] = 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:
require-sri-for
is described in subresource-integrityupgrade-insecure-requests
in Upgrade Insecure Requests W3C CRblock-all-mixed-content
in Mixed Content W3C CR
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
CSPNonce.apply
returns the nonce as a String or throws exception.CSPNonce.attr
returnsnonce="$nonce"
as a TwirlHtml
orHtml.empty
CSPNonce.attrMap
returnsMap("nonce" -> nonce)
orMap.empty
CSPNonce.get
returnsSome(nonce)
orNone
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(Symbol("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 = Symbol("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 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
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() {
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();
}
}
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.