SimpleNotes/src/features/SecurityReport.kt

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