How to modify immutable headers and add nonces to response header

Hello everyone!

My goal: To use CF workers to modify a CSP header by adding nonces on the fly by following this guide.

The issue: An error is thrown that immutable headers can’t be modified. I consulted CF worker docs and fell on this page relating to changing headers. I’m not very experienced in js and I’m having a hard time applying the doc’s guidance to the script I have here. Can anyone spot my mistake or suggest further reading or examples to troubleshoot with?

async function handleRequest(request) {

  let cspNonce = btoa(crypto.getRandomValues(new Uint32Array(2)))
  let newReq = new Request(req)
  newReq.headers.set('CSP-NONCE', cspNonce)
  let response = await fetch(newReq)

  const cspConfig = {
    ##My static CSP config##
  }

  function buildCspHeader(cspConfig, nonce = null) {
    let directives = []

    Object.keys(cspConfig).forEach(function(directive) {
      let values = Array.from(cspConfig[directive])
      values.forEach(function(value, key) {
        if (nonce && value === "'nonce'") {
          values[key] = "'nonce-" + nonce + "'"
        } else if (nonce === null && value === "'nonce'") {
          values.splice(key, 1)
        }
      })

      if (values.length === 0) {
        directives.push(directive)
      } else {
        directives.push(directive + ' ' + values.join(' '))
      }
    })

    return directives.join('; ')
  }

  let newResponseHeaders = response.headers
  newResponseHeaders.set('Content-Security-Policy-Report-Only', buildCspHeader(cspConfig, cspNonce))

  let init = {
    headers: newResponseHeaders,
    status: response.status,
    statusText: response.statusText
    }

  return new Response(response.body, init)
}
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

I’ve got the worker “working”! Working in that it no longer returns a 500 error, but it isn’t actually accomplishing it’s goal.

The worker was supposed to detect and change every nonce="<?= html_escape($cspNonce); ?>" into an actually randomly generated nonce and include it in the returned CSP header. The worker currently returns the page with the static CSP header, but only picks up two of the nonces.

Any ideas on how to get the script to iterate for every single nonce="<?= html_escape($cspNonce); ?>" it encounters on an html page and add it to the CSP header?

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {

  let cspNonce = btoa(crypto.getRandomValues(new Uint32Array(2)))
  request = new Request(request)
  request.headers.set('CSP-NONCE', cspNonce)
  let response = await fetch(request)

  const cspConfig = {
    "default-src": [
      "'none'",
    ],
    "img-src": [
      "www.google-analytics.com",
      "www.google.ca",
      "www.google.com",
      "www.googletagmanager.com",
      "avatars1.githubusercontent.com",
      "stats.g.doubleclick.net",
      "'self'",
    ],
    "script-src-elem": [
      "www.google-analytics.com",
      "www.google.ca",
      "www.google.com",
      "www.googletagmanager.com",
      "static.cloudflareinsights.com",
      "ajax.cloudflare.com",
      "www.gstatic.com",
      "unpkg.com",
      "''strict-dynamic'",
      "'self'",
      "'nonce'",
    ],
    "style-src-elem": [
      "fonts.googleapis.com",
      "unpkg.com",
      "fonts.googleapis.com",
      "'self'",
      "'nonce'",
    ],
    "worker-src": [
      "'self'",
    ],
    "manifest-src": [
      "'self'",
    ],
    "frame-src": [
      "www.google.com",
    ],
    "font-src": [
      "fonts.gstatic.com",
      "'self'",
    ],
    "connect-src": [
      "www.google-analytics.com",
      "www.googletagmanager.com",
      "www.google.ca",
      "www.google.com",
      "stats.g.doubleclick.net",
      "api.github.com",
      "cors-anywhere.herokuapp.com",
      "'self'",
    ],
  }

  function buildCspHeader(cspConfig, nonce = null) {
    let directives = []

    Object.keys(cspConfig).forEach(function(directive) {
      let values = Array.from(cspConfig[directive])
      values.forEach(function(value, key) {
        if (nonce && value === "'nonce'") {
          values[key] = "'nonce-" + nonce + "'"
        } else if (nonce === null && value === "'nonce'") {
          values.splice(key, 1)
        }
      })

      if (values.length === 0) {
        directives.push(directive)
      } else {
        directives.push(directive + ' ' + values.join(' '))
      }
    })

    return directives.join('; ')
  }

  response = new Response(response.body, response)
  response.headers.set('Content-Security-Policy-Report-Only', buildCspHeader(cspConfig, cspNonce))

  let init = {
    headers: response,
    status: response.status,
    statusText: response.statusText
    }

  return response
}

What does the fetch response look like?

If it’s just at text body (HTML/XML) and not huge, you could just do a regex match and then iterate on the match-array to do a search and replace.

Thanks @thomas4,

I’m going to have to go back to the drawing board on this: the “content-security-policy” header includes the nonce (and only the one) where each placeholder ‘nonce’ was in the constant “cspConfig”. That part isn’t an issue, I think, since one would suffice.

The real issue is that the source HTML comes through without the “<?= html_escape($cspNonce); ?>” being replaced by that very nonce. The script (from what I understand) is missing the component that replaces strings on the response body.