Skip to content

Commit

Permalink
Fixes #25032: Use Content-Security-Policy strict headers in utilities…
Browse files Browse the repository at this point in the history
… pages
  • Loading branch information
clarktsiory committed Jul 5, 2024
1 parent 4ef5430 commit 4f3457b
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import com.normation.rudder.users.CurrentUser
import com.normation.rudder.users.RudderUserDetail
import com.normation.rudder.web.snippet.CustomPageJs
import com.normation.rudder.web.snippet.WithCachedResource
import com.normation.rudder.web.snippet.WithEnabledCSP
import com.normation.rudder.web.snippet.WithNonce
import com.normation.utils.DateFormaterService
import com.normation.zio.*
Expand All @@ -92,7 +93,6 @@ import org.springframework.security.core.context.SecurityContextHolder
import scala.concurrent.duration.DAYS
import scala.concurrent.duration.Duration
import scala.util.chaining.*
import scala.util.matching.Regex
import scala.xml.Elem
import scala.xml.Node
import scala.xml.NodeSeq
Expand All @@ -104,26 +104,11 @@ import zio.syntax.*
*/
object Boot {

object RequestHeadersFactoryVendor {

/**
* A list of uris to match against when deciding whether to add a nonce to the CSP header for strict CSP
* (see level 3 specification section for nonce : https://www.w3.org/TR/CSP3/#framework-directive-source-list).
*
* This will apply our custom CSP headers for all html tags within the page with the `with-nonce` snippet directive.
*/
val customCSPUrisRegexList: List[Regex] = List(
"^.*/secure/utilities/healthcheck$" // healthcheck: main page
).map(_.r)

}

/**
* A vendor for our custom headers.
* We use it as default vendor for headers with our custom routing logic of CSP headers for instance.
*/
final class RequestHeadersFactoryVendor(csp: ContentSecurityPolicy) extends Vendor[List[(String, String)]] {
import RequestHeadersFactoryVendor.*

LiftRules.registerInjection(this)

Expand All @@ -140,32 +125,30 @@ object Boot {
* Returns all headers depending on page url, using current request nonce and add all other initial CSP directives
*/
private def addCspHeaders(allHeaders: List[(String, String)]): List[(String, String)] = {
S.uri match {
case uri if customCSPUrisRegexList.exists(_.matches(uri)) => {
val nonce = WithNonce.getCurrentNonce

val cspHeader = compileCSPHeader(
cspDirectives
.pipe(
replaceCSPRestrictionDirectives("script-src", s"'nonce-${nonce}' 'strict-dynamic'")(_)
)
.pipe(
replaceCSPRestrictionDirectives("object-src", "'none'")(_)
)
.pipe(
_ :+ ("base-uri" -> "'none'") :+ ("report-uri" -> s"${S.contextPath}/${LiftRules.liftContextRelativePath}/content-security-policy-report")
)
)
val newCspHeaders = csp
.headers()
.collect {
// replace all content security policies directives
case (header, _) if cspHeaderNames.contains(header) => header -> cspHeader
}
newCspHeaders ++ allHeaders.filterNot(h => cspHeaderNames.contains(h._1))
}
case _ =>
allHeaders // no headers to override
if (WithEnabledCSP.isEnabled) {
val nonce = WithNonce.getCurrentNonce

val cspHeader = compileCSPHeader(
cspDirectives
.pipe(
replaceCSPRestrictionDirectives("script-src", s"'nonce-${nonce}' 'strict-dynamic'")(_)
)
.pipe(
replaceCSPRestrictionDirectives("object-src", "'none'")(_)
)
.pipe(
_ :+ ("base-uri" -> "'none'") :+ ("report-uri" -> s"${S.contextPath}/${LiftRules.liftContextRelativePath}/content-security-policy-report")
)
)
val newCspHeaders = csp
.headers()
.collect {
// replace all content security policies directives
case (header, _) if cspHeaderNames.contains(header) => header -> cspHeader
}
newCspHeaders ++ allHeaders.filterNot(h => cspHeaderNames.contains(h._1))
} else {
allHeaders // no headers to override
}
}

Expand Down Expand Up @@ -597,7 +580,7 @@ class Boot extends Loggable {
ContentSourceRestriction.Self :: ContentSourceRestriction.UnsafeInline :: ContentSourceRestriction.UnsafeEval :: Nil
)

LiftRules.snippetDispatch.append(Map("with-nonce" -> WithNonce))
LiftRules.snippetDispatch.append(Map("with-nonce" -> WithNonce, "with-enabled-csp" -> WithEnabledCSP))
LiftRules.securityRules = () => {
SecurityRules(
https = hsts,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.normation.rudder.web.snippet

import net.liftweb.http.RequestVar
import net.liftweb.http.StatefulSnippet
import scala.xml.*

object WithEnabledCSP extends StatefulSnippet {

/**
* Holder for state of strict CSP headers activation
*/
private object enabled extends RequestVar[Boolean](false) {}

def dispatch: PartialFunction[String, NodeSeq => NodeSeq] = _ => enable()

def enable(): NodeSeq => NodeSeq = {
enabled.setIfUnset(true)
identity
}

def isEnabled: Boolean = enabled.get

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<lift:surround with="common-layout" at="content">
<lift:surround data-lift="with-enabled-csp" with="common-layout" at="content">

<head>
<title>Rudder - Archives</title>
<title>Rudder - Archives - Blabla</title>
<style>
.rudder-template > .one-col{
flex: 1;
Expand All @@ -26,7 +26,7 @@
}
</style>

<script>
<script data-lift="with-nonce">
//<![CDATA[
function enableIfNonEmpty(selectId, buttonId) {
$("#"+selectId).change(function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<lift:surround with="common-layout" at="content">
<lift:surround data-lift="with-enabled-csp" with="common-layout" at="content">
<lift:authz role="administration_read">
<div id="healthcheck-main">
<head_merge>
Expand Down

0 comments on commit 4f3457b

Please sign in to comment.