I created a site using the new Google sites (not Classic Sites), set up site proxying through Cloudflare, and enabled the Email Address Obfuscation feature in Cloudflare. Then I added a button that performs a simple action mailto:info@example.com
and ran into a problem: When I click on the button, I am taken to the Cloudflare "Email Protection" page with the message "You are unable to access this email address example.com".
This is for a simple reason - my browser (and it will happen with any modern browser) does not load the email-decode.min.js
script from Cloudflare. In turn, this is due to the fact that Google Sites uses CSP >= v2 and the CSP directives are configured in such a way that they do not allow the script from Cloudflare to load.
According to Cloudflare documentation, in order to use Scrape Shield you need to update CSP headers as follows:
script-src 'self' 'unsafe-inline'
This is what the new Google Sites CSP header looks like:
base-uri 'self';
object-src 'none';
report-uri /_/view/cspreport;
script-src 'report-sample' 'nonce-7+8CsMF6KihKnNmDwfM84w' 'unsafe-inline' 'unsafe-eval';
worker-src 'self';
frame-ancestors https://google-admin.corp.google.com/
* nonce-<base64-value>
is updated with every request.
When loading a page that contains email I see the following error in the browser console:
Refused to load the script 'https://example.com/cdn-cgi/scripts/6d6ddgh8/cloudflare-static/email-decode.min.js' because it violates the following Content Security Policy directive: "script-src 'report-sample' 'nonce-7+8CsMF6KihKnNmDwfM84w' 'unsafe-inline' 'unsafe-eval'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
I'm not sure about the reason, but the script won't load for one of two reasons:
'self'
is not set for the script-src
directive (I don’t think this is the case).'unsafe-inline'
is ignored because a cryptographic nonce is present.It doesn't work anyway. I see two solutions, but I don't know how to implement them:
'self'
to the script-src
directive, or if I had the ability to customize this header and I would do it myself.'nonce-<base64-value>'
that the Google server returns when the site is requested and adds it to its scripts.It would be grateful if someone could share a solution to this problem.
So I figured out that the reason is the missing 'self'
source in the script-src
directive.
I found a forum thread that suggests using Cloudflare Workers to change the required data in a request / response on the fly. I also found a ready-made example code for a worker that allows to replace headers in the request / response.
Inspired by this idea and given that Cloudflare provides 100,000 requests per day for free, I wrote and deployed a worker code that changes the server response headers, in fact, it updates the script-src
directive in the content-security-policy
header, supplementing it with the sources specified in the variable sources
.
My problem is solved, now the Cloudflare script is loading and the button is working.
* I don't promise quality code, but it works. The code could be made even more versatile, but I didn't have time for that.
Here is my worker code:
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request))
})
const sources = ["'self'"]
/**
* The function to update a CSP directive with new sources.
* @param {string} directive CSP directive.
* @param {string[]} sources Sources to add to the directive.
* @return {string} Updated CSP directive.
*/
function updateDirective(directive, sources) {
for (let i = 0; i < sources.length; i++) {
if (!directive.toLowerCase().includes(sources[i])) {
directive = directive.concat(" ", sources[i])
}
}
return directive
}
/**
* The function to update the Content-Security-Policy header.
* @param {string} header The Content-Security-Policy header.
* @param {string} directive The Content-Security-Policy directive whose sources need to be updated.
* @param {string} sources Sources to add to the directive.
* @return {string} Updated Content-Security-Policy header.
*/
function updateHeader(header, directive, sources) {
let sourceHeader = header.split(';')
let updatedHeader = []
for (let i = 0; i < sourceHeader.length; i++) {
if (sourceHeader[i].includes(directive)) {
updatedHeader.push(updateDirective(sourceHeader[i], sources))
} else {
updatedHeader.push(sourceHeader[i])
}
}
return updatedHeader.join(";")
}
async function handleRequest(request) {
let response = await fetch(request)
response = new Response(response.body, response)
response.headers.set('content-security-policy',
updateHeader(response.headers.get('content-security-policy'), "script-src", sources))
return response
}