_headers different CSP single URL

I am trying to have a different CSP header on a specific page but keep another CSP for all other pages.

Here’s what I have tried:

/*
    Content-Security-Policy: default-src 'self';base-uri 'none';font-src 'self';script-src 'self' https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data:;img-src 'self' data: https://www.google-analytics.com;object-src 'none'; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    report-to: {\"group\":\"default\",\"max_age\":31536000,\"endpoints\":[{\"url\":\"https://somedomain.report-uri.com/a/d/g\"}],\"include_subdomains\":true}
    Permissions-Policy: document-domain=(), accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
    Strict-Transport-Security: max-age=2592000
    X-XSS-Protection: 1; mode=block
    X-Frame-Options: SAMEORIGIN
    X-Permitted-Cross-Domain-Policies: none

/search.html
    Content-Security-Policy: default-src 'self';base-uri 'none';font-src 'self';script-src 'self' 'unsafe-eval' https://*.google.com/ https://cse.google.com/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data: https://www.google.com/;img-src 'self' data:  https://*.gstatic.com/ https://*.google.com/ https://www.googleapis.com/ https://www.google-analytics.com;object-src 'none'; frame-src 'self' https://cse.google.com/; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests

I have also tried inverting the order of the 2 declarations but the result is always the same: only the /* is recognised. Not the /search.html one…

What am I doing wrong?

I read the page that describes how this works here: https://developers.cloudflare.com/pages/platform/headers

Hey,

We remove the “.html” at the end of pages, so visiting your site you’d actually go to /search rather than /search.html
If you remove the “.html” suffix, this should work. Let me know how it goes!

Hi @Walshy thank you for your reply!

I have removed the .html but unfortunately that did not seem to make a difference…

Here’s the current content of my _headers file:

/search
    Content-Security-Policy: default-src 'self';base-uri 'none';font-src 'self';script-src 'self' 'unsafe-eval' https://gtm.generation.global/ https://*.google.com/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://generation.global https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data: https://www.google.com/;img-src 'self' data:  https://*.gstatic.com/ https://*.google.com/ https://www.googleapis.com/ https://www.google-analytics.com;object-src 'none'; frame-src 'self' https://cse.google.com/; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests

/*
    Content-Security-Policy: default-src 'self';base-uri 'none';font-src 'self';script-src 'self' https://gtm.generation.global/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://generation.global https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data:;img-src 'self' data: https://www.google-analytics.com;object-src 'none'; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    report-to: {\"group\":\"default\",\"max_age\":31536000,\"endpoints\":[{\"url\":\"https://generationglobal.report-uri.com/a/d/g\"}],\"include_subdomains\":true}
    Permissions-Policy: document-domain=(), accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
    Strict-Transport-Security: max-age=2592000
    X-XSS-Protection: 1; mode=block
    X-Frame-Options: SAMEORIGIN
    X-Permitted-Cross-Domain-Policies: none

This is the page Search | Generation Global

It looks good to me:

$ curl -sv https://generation.global/search 2>&1 | rg 'content-security-policy'
< content-security-policy: default-src 'self';base-uri 'none';font-src 'self';script-src 'self' 'unsafe-eval' https://gtm.generation.global/ https://*.google.com/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://generation.global https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data: https://www.google.com/;img-src 'self' data:  https://*.gstatic.com/ https://*.google.com/ https://www.googleapis.com/ https://www.google-analytics.com;object-src 'none'; frame-src 'self' https://cse.google.com/; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests, default-src 'self';base-uri 'none';font-src 'self';script-src 'self' https://gtm.generation.global/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://generation.global https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data:;img-src 'self' data: https://www.google-analytics.com;object-src 'none'; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests

It’s very strange. I have purged CF’s and my browser’s cache, also tried with another browser in a private window. But I still see this in my console (it loads the other CSP):

But then in the network tab I see the correct CSP:

I really don’t know what’s going on

Oh it’s appending the second CSP to the first!

If that’s the case how could I avoid a duplicate CSP but only add the required declarations…?

Ah yep I see it adding to itself. I was wondering if this would be an issue…

I can’t think of a good way to really solve this without using something like Functions. You could potentially have the /* first and then just add the extra properties in the second rule (which will be appended) - that may solve it but not nicely.

I’d definitely say Functions are the best way to go but wonder if there’s anything we can do to improve that. Will need to think about it

I have found a workaround, using Workers.

let addHeaders = {
  "Content-Security-Policy": "default-src 'self';base-uri 'none';font-src 'self';script-src 'self' 'unsafe-eval' https://*.google.com/ https://gtm.generation.global/ https://www.google-analytics.com https://ajax.cloudflare.com https://static.cloudflareinsights.com https://cc.cdn.civiccomputing.com;connect-src https://www.google-analytics.com https://generation.global https://apikeys.civiccomputing.com https://clapi.civiccomputing.com/;style-src 'self' 'unsafe-inline' data: https://www.google.com/;img-src 'self' data:  https://*.gstatic.com/ https://*.google.com/ https://www.googleapis.com/ https://www.google-analytics.com;object-src 'none'; frame-src 'self' https://cse.google.com/; form-action 'self' https://*.list-manage.com; upgrade-insecure-requests",
}

let removeHeaders = [
//  "Server",
  "X-Powered-By",
  "Content-Security-Policy"
//  "X-Generator"
]

addEventListener("fetch", event => {
  event.respondWith(fetchAndApply(event.request))
})

async function fetchAndApply(request) {
  // Fetch the original page from the origin
  let response = await fetch(request)

  // Make response headers mutable
  response = new Response(response.body, response)

  // Delete each header in removeHeaders
  removeHeaders.forEach(function(name){
    response.headers.delete(name)
  })

  // Set each header in addHeaders
  Object.keys(addHeaders).map(function(name, index) {
    response.headers.set(name, addHeaders[name])
  })

  // Return the new mutated page
  return response
}

I remove the CSP added by _headers and add a new one