
If you have a website powered by NodeJS and Express, using Helmet is an easy way to add a layer of protection for your site and more privacy for your visitors by setting various HTTP headers. But it can also break things, one of them being Fathom Analytics.
As with other remote-hosted scripts, your site will reach across the web to fetch and run JavaScript from another server. This can be used for good or evil. Privacy-focused analytics are firmly in the “good” camp, so here’s how to set headers on your site for protection while allowing Fathom through.
// Will not work – too strict:
app.use(helmet());
Helmet sets 15 headers which you can run one-by-one, so with trial and error we can find which ones are blockers. Spoiler alert – it’s these…
Content Security Policy (CSP)
First up is contentSecurityPolicy()
, the method dealing with CSP directives. Tweaking this will fix “refused to load” errors. By default, Helmet’s CSP only allows self-domain scripts to run so we need to allow the domain that’s hosting the analytics script, as well as data:
schemes.
If you’ve set a custom domain for the script (in the “Script Settings” page of your Fathom dashboard), then you need to specify that, including the subdomain. If not, use cdn.usefathom.com
instead. In other words, whatever subdomain+domain you’re using in your embed code, add that to the CSP directives like this:
// Change cdn.usefathom.com to your custom domain if necessary.
app.use(helmet.contentSecurityPolicy({
directives: {
"script-src": ["'self'", "data:", "cdn.usefathom.com"],
"img-src": ["'self'", "data:", "cdn.usefathom.com"]
}
}));
Cross-Origin Embedder Policy (COEP)
The next error you’ll get is NotSameOriginAfterDefaultedToSameOriginByCoep
. Quite a mouthful but thankfully the fix is shorter than the error message.
Ideally we’d allow cross-origin embedding just for Fathom but sadly we can’t allow individual domains. The next best thing is to use a policy value of credentialless
, which will allow embedding as long as credentials (i.e. cookies, authorization headers, or TLS client certificates) are not needed. This is enough for our purposes, so here’s how we set it:
app.use(helmet.crossOriginEmbedderPolicy({
policy: "credentialless"
}));
Referrer Policy
The final fix is only necessary if you use a custom domain for the analytics script and you’ve added any “allowed domains” in your Fathom settings. This will produce 403 “Forbidden” errors because Helmet hides referrer information and therefore the allowed domains can’t be detected. Thanks to Nils Mielke for going down the rabbit-hole and sharing this discovery.
To fix this, we can change the policy to hide detailed referrer information except the referrer origin (i.e. your site’s domain name). In other words, when visitors click on your link to visit another website, that site will know they came from your domain but won’t know the specific page they came from.
app.use(helmet.referrerPolicy({
policy: "strict-origin-when-cross-origin"
}));
I find this to be a good compromise that respects user privacy, however if you’d prefer to hide all referrer information then you should either allow the Fathom script to run on any domain (i.e. delete any “allowed domains”) or don’t use a custom script domain – use cdn.usefathom.com
instead.
Putting It All together
The above code snippets are for when you set each of Helmet’s headers individually, but a more compact way is to run Helmet once and just override the individual header options as needed, like this:
// Change cdn.usefathom.com to your custom domain if necessary.
app.use(
helmet({
contentSecurityPolicy: {
directives: {
"script-src": ["'self'", "data:", "cdn.usefathom.com"],
"img-src": ["'self'", "data:", "cdn.usefathom.com"]
}
},
crossOriginEmbedderPolicy: {
policy: "credentialless"
},
referrerPolicy: {
policy: "strict-origin-when-cross-origin"
}
})
);
Don’t forget to tweak this code depending on your setup, e.g. you might not need to set the Referrer Policy. In any case, that should be everything you need to get Fathom Analytics running on an Express app using Helmet, or indeed on any site that sets custom headers for better security and privacy.
Further Reading
Mozilla Developer Network has good guides to the individual headers discussed: