Content Security Policy being overwritten

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.

One comment, probably not related to the issue, if you use Meteor 3:

WebApp.rawHandlers.use((req, res, next) => {})

(Breaking changes | Meteor 3.0 Migration Guide)

1 Like

When the BrowserPolicy package is used, it sets the CSP based on the config set utilizing it.

1 Like

Ah, good call, thanks. We’re not 3.0 ready yet but good to know.

Gotcha. I hate that it’s kind of a black box, as I prefer to set CSPs across apps in specific ways. I tried to remove browser-policy altogether in .meteor/packages but that broke the app, so I’m not sure it can be removed, unfortunately. Do you know if there’s a way to remove the package and still have a usable Meteor app?

I don’t remember that package being required as a core Meteor package.