From 2effb99da948623ceaece86124ed04a5d8f8552e Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Mon, 25 Mar 2024 13:02:18 +0000 Subject: [PATCH] replace some hard coded clauses of the Content-Security-Policy with values from config to remove guardian specific stuff --- dev/script/generate-config/service-config.js | 4 +- kahuna/app/KahunaComponents.scala | 44 ++++++++++++++------ kahuna/app/lib/KahunaConfig.scala | 15 ++++++- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/dev/script/generate-config/service-config.js b/dev/script/generate-config/service-config.js index 14020de1a5..f34ad2abf2 100644 --- a/dev/script/generate-config/service-config.js +++ b/dev/script/generate-config/service-config.js @@ -90,7 +90,9 @@ function getKahunaConfig(config){ | host: "https://pinboard.${config.DOMAIN}", | path: "pinboard.loader.js", | async: true, - | permission: "pinboard" + | permission: "pinboard", + | additionalFrameSourcesForCSP: ["https://www.youtube.com"], + | additionalImageSourcesForCSP: ["https://*.googleusercontent.com"] | } |]`; diff --git a/kahuna/app/KahunaComponents.scala b/kahuna/app/KahunaComponents.scala index 12ad004132..34eca50c58 100644 --- a/kahuna/app/KahunaComponents.scala +++ b/kahuna/app/KahunaComponents.scala @@ -24,7 +24,20 @@ object KahunaSecurityConfig { def apply(config: KahunaConfig, playConfig: Configuration): SecurityHeadersConfig = { val base = SecurityHeadersConfig.fromConfiguration(playConfig) - val services = List( + def cspClause(directive: String, sources: Set[String]): String = s"$directive ${sources.mkString(" ")}" + + val frameSources = cspClause("frame-src", Set( + config.services.authBaseUri, + config.services.kahunaBaseUri) + ++ config.frameSources + ++ config.scriptsToLoad.flatMap(_.cspFrameSources) + ) + val frameAncestors = cspClause("frame-ancestors", + config.frameAncestors + ) + val connectSources = cspClause("connect-src", Set( + "'self'", + config.imageOrigin, config.services.apiBaseUri, config.services.loaderBaseUri, config.services.cropperBaseUri, @@ -34,28 +47,33 @@ object KahunaSecurityConfig { config.services.collectionsBaseUri, config.services.leasesBaseUri, config.services.authBaseUri, - config.services.guardianWitnessBaseUri + config.services.guardianWitnessBaseUri) + ++ config.connectSources ) - val frameSources = s"frame-src ${config.services.authBaseUri} ${config.services.kahunaBaseUri} https://accounts.google.com https://www.youtube.com ${config.scriptsToLoad.map(_.host).mkString(" ")}" - val frameAncestors = s"frame-ancestors ${config.frameAncestors.mkString(" ")}" - val connectSources = s"connect-src 'self' ${(services :+ config.imageOrigin).mkString(" ")} ${config.connectSources.mkString(" ")}" - - val imageSources = s"img-src ${List( + val imageSources = cspClause("img-src", Set( + "'self'", "data:", "blob:", URI.ensureSecure(config.services.imgopsBaseUri).toString, URI.ensureSecure(config.fullOrigin).toString, URI.ensureSecure(config.thumbOrigin).toString, URI.ensureSecure(config.cropOrigin).toString, - URI.ensureSecure("app.getsentry.com").toString, - "https://*.googleusercontent.com", - "'self'" - ).mkString(" ")} ${config.imageSources.mkString(" ")}" + URI.ensureSecure("app.getsentry.com").toString) + ++ config.scriptsToLoad.flatMap(_.cspImageSources) + ++ config.imageSources + ) - val fontSources = s"font-src data: 'self' ${config.fontSources.mkString(" ")}" + val fontSources = cspClause("font-src", Set( + "'self'") + ++ config.fontSources + ) - val scriptSources = s"script-src 'self' 'unsafe-inline' ${config.scriptsToLoad.map(_.host).mkString(" ")}" + val scriptSources = cspClause("script-src", Set( + "'self'", + "'unsafe-inline'") + ++ config.scriptsToLoad.flatMap(_.cspScriptSources) + ) base.copy( // covered by frame-ancestors in contentSecurityPolicy diff --git a/kahuna/app/lib/KahunaConfig.scala b/kahuna/app/lib/KahunaConfig.scala index 82953614d1..9c7f83f241 100644 --- a/kahuna/app/lib/KahunaConfig.scala +++ b/kahuna/app/lib/KahunaConfig.scala @@ -4,13 +4,21 @@ import com.gu.mediaservice.lib.auth.Permissions.Pinboard import com.gu.mediaservice.lib.auth.SimplePermission import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources} +import scala.jdk.CollectionConverters.iterableAsScalaIterableConverter + case class ScriptToLoad( host: String, path: String, async: Option[Boolean], permission: Option[SimplePermission], - shouldLoadWhenIFramed: Option[Boolean] -) + shouldLoadWhenIFramed: Option[Boolean], + additionalFrameSourcesForCSP: Option[Set[String]] = None, + additionalImageSourcesForCSP: Option[Set[String]] = None, +) { + def cspScriptSources: Set[String] = Set(host) + def cspFrameSources: Set[String] = additionalFrameSourcesForCSP.getOrElse(Set.empty) + host + def cspImageSources: Set[String] = additionalImageSourcesForCSP.getOrElse(Set.empty) +} class KahunaConfig(resources: GridConfigResources) extends CommonConfig(resources) { val rootUri: String = services.kahunaBaseUri @@ -41,6 +49,7 @@ class KahunaConfig(resources: GridConfigResources) extends CommonConfig(resource val showDenySyndicationWarning: Option[Boolean] = booleanOpt("showDenySyndicationWarning") + val frameSources: Set[String] = getStringSet("security.frameSources") val frameAncestors: Set[String] = getStringSet("security.frameAncestors") val connectSources: Set[String] = getStringSet("security.connectSources") ++ maybeIngestBucket.map { ingestBucket => if (isDev) "https://localstack.media.local.dev-gutools.co.uk" @@ -56,6 +65,8 @@ class KahunaConfig(resources: GridConfigResources) extends CommonConfig(resource // FIXME ideally the below would not hardcode reference to pinboard - hopefully future iterations of the pluggable authorisation will support evaluating permissions without a corresponding case object permission = if (entry.hasPath("permission") && entry.getString("permission") == "pinboard") Some(Pinboard) else None, shouldLoadWhenIFramed = if (entry.hasPath("shouldLoadWhenIFramed")) Some(entry.getBoolean("shouldLoadWhenIFramed")) else None, + additionalFrameSourcesForCSP = if (entry.hasPath("additionalFrameSourcesForCSP")) Some(entry.getStringList("additionalFrameSourcesForCSP").asScala.toSet) else None, + additionalImageSourcesForCSP = if (entry.hasPath("additionalImageSourcesForCSP")) Some(entry.getStringList("additionalImageSourcesForCSP").asScala.toSet) else None, )) val metadataTemplates: Seq[MetadataTemplate] = configuration.get[Seq[MetadataTemplate]]("metadata.templates")