Need some guidance on why/how CSPs are being overwritten. We are just about to enter beta testing for a new Meteor web app, and after updating some browser policy directives and deploying to Digital Ocean (using the zodern/meteor:root Docker image), we suddenly got 502 Bad Gateway errors. No nginx config was changed, nor Meteor configs, just some client side files and a server side file for browser policy.
This is how the browser-policy.js
file looked:
import { BrowserPolicy } from 'meteor/browser-policy';
import { WebApp } from 'meteor/webapp';
BrowserPolicy.framing.disallow();
BrowserPolicy.content.disallowInlineScripts();
BrowserPolicy.content.disallowEval();
BrowserPolicy.content.allowInlineStyles();
BrowserPolicy.content.allowFontDataUrl();
var trusted = [
'*.googleapis.com',
'*.cloudflare.com',
'*.gstatic.com',
'ajax.cloudflare.com',
'*.lr-ingest.io',
'blob:',
'*.jsdelivr.net',
'*.stripe.com',
'res.cloudinary.com',
'cdn.lrkt-in.com',
'https://www.google.com'
];
trusted.forEach(function(origin) {
BrowserPolicy.content.allowOriginForAll(origin);
});
Meteor.startup(() => {
WebApp.rawConnectHandlers.use((req, res, next) => {
// Cache control
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
// Prevent Adobe stuff loading content on our site
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
// Frameguard - https://helmetjs.github.io/docs/frameguard/
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('frame-ancestors', 'deny');
// X-XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// No content sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// DNS pre-fetching
res.setHeader('X-DNS-Prefetch-Control', 'off');
// Expect CT
res.setHeader('Expect-CT', 'enforce, max-age=604800');
// Links referrer policy
res.setHeader('Referrer-Header', 'same-origin');
// Prevent IE from executing downloads in page content
res.setHeader('X-Download-Options', 'noopen');
// Content security policy
const csp = [
`default-src 'self' data:;`,
`connect-src blob:;`,
`child-src 'self' blob:;`,
`frame-src 'self' https://www.google.com;`,
`img-src 'self' https://res.cloudinary.com;`,
`style-src-elem 'self' https://fonts.googleapis.com;`,
`worker-src 'self' blob:;`,
`script-src 'self' blob: data: https://js.stripe.com https://cdn.logrocket.io https://cdn.lrkt-in.com;`,
`connect-src https://*.logrocket.io;`
];
res.setHeader('Content-Security-Policy', csp.join(' '));
next();
});
});
All that changed were a couple more urls added to the trusted
array. Once deployed, we got the 502 error. Remove the changes, and the site came back up. Really strange.
I’ve seen forum posts here dating to 2020 where folks indicated that the browser-policy package is out of date, so I removed the import/usage to instead focus on setting CSPs through WebApp.rawConnectHandlers.use((req, res, next) => {
So now the file looks like:
import { WebApp } from 'meteor/webapp';
Meteor.startup(() => {
WebApp.rawConnectHandlers.use((req, res, next) => {
// Cache control
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
// Prevent Adobe stuff loading content on our site
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
// Frameguard - https://helmetjs.github.io/docs/frameguard/
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('frame-ancestors', 'deny');
// X-XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// No content sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// DNS pre-fetching
res.setHeader('X-DNS-Prefetch-Control', 'off');
// Expect CT
res.setHeader('Expect-CT', 'enforce, max-age=604800');
// Links referrer policy
res.setHeader('Referrer-Header', 'same-origin');
// Prevent IE from executing downloads in page content
res.setHeader('X-Download-Options', 'noopen');
// Content security policy
const csp = [
`default-src 'self' data:;`,
`connect-src blob:;`,
`child-src 'self' blob:;`,
`frame-src 'self' https://www.google.com;`,
`img-src 'self' https://res.cloudinary.com;`,
`style-src-elem 'self' https://fonts.googleapis.com;`,
`worker-src 'self' blob:;`,
`script-src 'self' blob: data: https://js.stripe.com https://cdn.logrocket.io https://cdn.lrkt-in.com;`,
`connect-src https://*.logrocket.io;`
];
res.setHeader('Content-Security-Policy', csp.join(' '));
next();
});
});
Very simple and all the headers are properly set…except the CSPs(!). I cannot figure out what is overriding them. If I rename the header to something like res.setHeader('Content-Security-Policy-FOO', csp.join(' '));
, then my FOO header with the above CSP directives is there, so I know this file is properly writing headers, but the default Content-Security-Policy
header is still there with directives I can’t seem to override.
Would love if anyone has some insight on this. I’d rather not use Helmet if I can avoid the dependency.