112 lines
4.3 KiB
Kotlin
112 lines
4.3 KiB
Kotlin
package be.vandewalleh.features
|
|
|
|
import be.vandewalleh.extensions.ApplicationBuilder
|
|
import com.fasterxml.jackson.databind.JsonNode
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
import com.fasterxml.jackson.module.kotlin.readValue
|
|
import io.ktor.application.*
|
|
import io.ktor.http.*
|
|
import io.ktor.http.content.*
|
|
import io.ktor.request.*
|
|
import io.ktor.response.*
|
|
import io.ktor.routing.*
|
|
import io.ktor.util.*
|
|
import org.slf4j.LoggerFactory
|
|
import kotlin.collections.HashMap
|
|
|
|
class SecurityReport : ApplicationBuilder({
|
|
|
|
val logger = LoggerFactory.getLogger(SecurityReport::class.java)
|
|
|
|
routing {
|
|
post("/csp-reports") {
|
|
val txt = call.receiveText()
|
|
val json = ObjectMapper().registerModule(KotlinModule()).readValue<JsonNode>(txt)
|
|
logger.info(json.toPrettyString())
|
|
call.response.status(HttpStatusCode.OK)
|
|
}
|
|
}
|
|
|
|
install(SecurityHeaders) {
|
|
reportOnly = false
|
|
reportUri = "/csp-reports"
|
|
cspValue = "default-src 'self'"
|
|
}
|
|
})
|
|
|
|
class SecurityHeaders(configuration: Configuration) {
|
|
private val logger = LoggerFactory.getLogger(key.name)
|
|
private val headers = buildHeaders(configuration)
|
|
|
|
class Configuration {
|
|
var reportOnly = true
|
|
var reportUri = "/csp-reports"
|
|
|
|
var csp = true
|
|
var cspValue = "default-src 'self'"
|
|
|
|
var noSniff = true
|
|
var referrerPolicy = true
|
|
var xssFilter = true
|
|
var frameguard = true
|
|
var featurePolicy = false
|
|
}
|
|
|
|
companion object Feature :
|
|
ApplicationFeature<ApplicationCallPipeline, SecurityHeaders.Configuration, SecurityHeaders> {
|
|
override val key = AttributeKey<SecurityHeaders>("SecurityHeaders")
|
|
|
|
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): SecurityHeaders {
|
|
|
|
val configuration = SecurityHeaders.Configuration().apply(configure)
|
|
|
|
val feature = SecurityHeaders(configuration)
|
|
|
|
pipeline.sendPipeline.intercept(ApplicationSendPipeline.After) { subject ->
|
|
if (subject !is OutgoingContent) return@intercept
|
|
val contentType = subject.contentType ?: return@intercept
|
|
if (!ContentType.Text.Html.match(contentType.withoutParameters())) return@intercept
|
|
if (call.request.path().startsWith("/api")) return@intercept
|
|
feature.process(call, contentType)
|
|
}
|
|
|
|
return feature
|
|
}
|
|
}
|
|
|
|
fun process(call: ApplicationCall, contentType: ContentType) {
|
|
headers.forEach { (name, value) ->
|
|
call.response.headers.append(name, value)
|
|
}
|
|
|
|
logger.debug("Added security headers")
|
|
}
|
|
|
|
private fun buildHeaders(cfg: Configuration): HashMap<String, String> {
|
|
val headers = HashMap<String, String>()
|
|
|
|
if (cfg.noSniff) headers["X-Content-Type-Options"] = "nosniff"
|
|
if (cfg.referrerPolicy) headers["Referrer-Policy"] = "no-referrer-when-downgrade"
|
|
if (cfg.xssFilter) headers["X-XSS-Protection"] = "1; mode=block"
|
|
if (cfg.frameguard) headers["X-Frame-Options"] = "DENY"
|
|
|
|
if (cfg.csp) {
|
|
val cspValue = if (cfg.reportOnly) cfg.cspValue + "; report-uri ${cfg.reportUri}" else cfg.cspValue
|
|
val cspKey = if (cfg.reportOnly) "Content-Security-Policy-Report-Only" else "Content-Security-Policy"
|
|
headers[cspKey] = cspValue
|
|
}
|
|
|
|
if (cfg.featurePolicy) {
|
|
|
|
// really ?? https://github.com/w3c/webappsec-feature-policy/issues/189
|
|
headers["Feature-Policy"] =
|
|
"accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; oversized-images 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; sync-xhr 'none'; unoptimized-images 'none'; unsized-media 'none'; usb 'none'; wake-lock 'none'; xr-spatial-tracking 'none'"
|
|
}
|
|
|
|
// TODO: Strict-Transport-Security: "max-age=31536000; includeSubDomains"
|
|
|
|
return headers
|
|
}
|
|
}
|