Content-Security-Policy builder tool in spring / java

The content security policy is a header that is used to coerce the resources that a webpage can read. It can be used to prevent malicious third party libraries, scripts, ads from capturing data or displaying content.

Many languages offer tools to read or parse the Content-Security-Policy format which is described formally in https://www.w3.org/TR/CSP2/.

But spring-security don’t offer any facility to build a policy with ease, so most of the time you will hardcode it in one place and update it globally.

I made a builder tool so you can workaround this limitation. The builder supports merging different policies, so you can declare fragments of policies in different places of your project.

Example :
default-src 'self', img-src https://site-1.com
+
default-src 'self', img-src https://site-2.com, script-src https://site-2.com
becomes
default-src 'self', img-src https://site-1.com https://site-2.com, script-src https://site-2.com

Just copy this tool in your classpath :

package com.mycompany.service.domain.contentsecurity

import org.springframework.boot.context.properties.ConfigurationProperties
import java.net.URI

@ConfigurationProperties("content-security-policy")
data class ContentSecurityPolicy(var definition: Map<String, String>? = null) {

fun get(): String =
(
definition
?: mapOf("root" to "default-src 'self'")
)
.toSortedMap { o1, o2 -> if (o1 == "root") -1 else if (o2 == "root") 1 else o1.compareTo(o2) }
.values.map { Rule.newContentSecurityPolicyRule((it ?: "").toString()) }
.reduce { acc, other -> acc + other }
.toString()

override fun toString() = get()

data class Rule(
val src: Map<String, Set<String>>,
val reportURI: URI? = null
) {

companion object {
private val NONE_VALUE = "'none'"
private val SELF = "'self'"
private val EVERYTHING = "*"
val NOTHING = Rule(src = mapOf())
val NONE = Rule(src = mapOf("default" to setOf(NONE_VALUE)))
val STRICT = Rule(src = mapOf("default" to setOf(SELF)))
val RELAXED = Rule(src = mapOf("default" to setOf(EVERYTHING)))
val regex = Regex("\\s*([a-z]+-src|report-uri)\\s+([^;]+)\\s*;")

fun newContentSecurityPolicyRule(plainText: String) = NOTHING + plainText
}

operator fun plus(other: Rule): Rule {
val categories = src.keys + other.src.keys
var new = mapOf<String, Set<String>>()
categories.forEach { category ->
val addedValues = (other.src[category] ?: emptySet())
val existingValues = (src[category] ?: emptySet())
val concatValues = existingValues + addedValues
val mergedValues = when {
(
existingValues.contains(EVERYTHING) &&
addedValues != setOf(SELF) &&
!addedValues.contains(NONE_VALUE)
) -> setOf(EVERYTHING)
addedValues.contains(EVERYTHING) -> setOf(EVERYTHING)
addedValues.contains(NONE_VALUE) -> setOf(NONE_VALUE)
addedValues == setOf(SELF) -> setOf(SELF)
else -> concatValues
}
new = new + (category to mergedValues)
}
return Rule(new, other.reportURI ?: this.reportURI)
}

operator fun plus(plainText: String): Rule {
var newMap = mapOf<String, Set<String>>()
var reportURI: URI? = null
regex.findAll("$plainText;")
.forEach {
if (it.groupValues[1] == "report-uri")
reportURI = URI(it.groupValues[2])
else {
val category = it.groupValues[1].replace(Regex("-src$"), "")
val addedValues = it.groupValues[2].split(" ").toSet()
newMap = newMap + (category to addedValues)
}
}
return this + Rule(newMap, reportURI)
}

override fun toString(): String {
val rules = src.entries.filter { it.value.isNotEmpty() }
.joinToString(";") { "${it.key}-src ${it.value.joinToString(" ")}" }
return "$rules${if (rules.isNotBlank()) ";" else ""}${
if (reportURI != null) "report-uri $reportURI;" else ""
}"
}
}
}

Then you will bind the “content-security-policy” in your application configuration to as many fragments you need.
root will take precedence over the rest so you can define app-wide rules, just don’t forget to add @EnableConfigurationProperties(ContentSecurityPolicy::class) to your @Configuration class

content-security-policy.definition.root: default-src 'self'
content-security-policy.definition.rule1: img-src https://site-1.com
content-security-policy.definition.rule2: img-src https://site-2.com, script-src https://site-2.com

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.