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(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 { override val key = AttributeKey("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 { val headers = HashMap() 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 } }