No ‘Access-Control-Allow-Origin’ header is present on the requested resource?

I’m using this on server startup to handle CORS for requests to my own server:

if (Meteor.isProduction){
    WebAppInternals.setBundledJsCssUrlRewriteHook(
        url => `https://xxxxxxx.cloudfront.net${url}&_g_app_v_=${process.env.GALAXY_APP_VERSION_ID}`
    );

    WebApp.rawConnectHandlers.use((req, res, next) => {
        if (
            req._parsedUrl.pathname.match(
                /\.(ttf|ttc|otf|eot|woff|woff2|font\.css|css|js|scss)$/
            )
        ) {
            res.setHeader('Access-Control-Allow-Origin', 'https://www.myWebSite.com');
        }
        next();
    });
}

A service I’m using wants to bring in a font from its own servers and I’m getting a CORS error:

Access to font at ‘https://cdn.userway.org/widgetapp/bundles/udf/UserwayDyslexiaFont-Medium.woff’ from origin ‘https://www.empowerpeople.care’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I’ve looked for the correct syntax to modify the above code to add CORS permissions for cdn.userway.org to load a font, but I haven’t found it yet.

What is the correct way to modify the above code for this purpose?

I did follow the Meteor Guide for Content Security Policy. It was very easy and it did work like a charm. That includes using helmet.

In particular, to allow using fonts from another domain you would need to use the directive fontSrc.

1 Like

Thanks for pointing me in this direction. It looks like this will enhance the security of my site. So far it appears to be working, at least on localhost.

Here’s how I did it so far (if anyone sees any errors/improvements, please post them):

First I changed this line in the sample code:

helmet(helmetOptions)

…to:

WebApp.connectHandlers.use(helmet(helmetOptions))

Then I added every outside service to settings.json like this:

  "allowedOrigins": [
    "https://api.userway.org/",
    "https://beacon-v2.helpscout.net/",
    "https://cdn.lr-ingest.io/",
    "https://cdn.userway.org/",
    "https://dg8cu6dzhqz3l.cloudfront.net/",
    "https://firebasestorage.googleapis.com/",
    "https://fonts.googleapis.com",
    "https://fonts.gstatic.com/",
    "https://progressier.com/",
    "https://ucarecdn.com/"
  ],

Then I added allowedOrigins after just about every ...Src helmetOption like this:

imgSrc: [self, data, 'blob:'].concat(allowedOrigins),
manifestSrc: [self].concat(allowedOrigins),
mediaSrc: [self].concat(allowedOrigins),

Here’s the full code:


const self = '\'self\''
const data = 'data:'
const unsafeEval = '\'unsafe-eval\''
const unsafeInline = '\'unsafe-inline\''
const allowedOrigins = Meteor.settings.allowedOrigins

// create the default connect source for our current domain in
// a multi-protocol compatible way (http/ws or https/wss)
const url = Meteor.absoluteUrl()
const domain = url.replace(/http(s)*:\/\//, '').replace(/\/$/, '')
const s = url.match(/(?!=http)s(?=:\/\/)/) ? 's' : ''
const usesHttps = s.length > 0
const connectSrc = [
    self,
    `http${s}://${domain}`,
    `ws${s}://${domain}`
]

// Prepare runtime config for generating the sha256 hash
// It is important, that the hash meets exactly the hash of the
// script in the client bundle.
// Otherwise the app would not be able to start, since the runtimeConfigScript
// is rejected __meteor_runtime_config__ is not available, causing
// a cascade of follow-up errors.
const runtimeConfig = Object.assign(__meteor_runtime_config__, Autoupdate, {
    // the following lines may depend on, whether you called Accounts.config
    // and whether your Meteor app is a "newer" version
    accountsConfigCalled: true,
    isModern: true
})

// add client versions to __meteor_runtime_config__
Object.keys(WebApp.clientPrograms).forEach(arch => {
    __meteor_runtime_config__.versions[arch] = {
        version: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].version(),
        versionRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionRefreshable(),
        versionNonRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionNonRefreshable(),
        // comment the following line if you use Meteor < 2.0
        versionReplaceable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionReplaceable()
    }
})

const runtimeConfigScript = `__meteor_runtime_config__ = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(runtimeConfig))}"))`
const runtimeConfigHash = crypto.createHash('sha256').update(runtimeConfigScript).digest('base64')

const helmetOptions = {
    contentSecurityPolicy: {
        blockAllMixedContent: true,
        directives: {
            defaultSrc: [self],
            scriptSrc: [
                self,
                // Remove / comment out unsafeEval if you do not use dynamic imports
                // to tighten security. However, if you use dynamic imports this line
                // must be kept in order to make them work.
                unsafeEval,
                `'sha256-${runtimeConfigHash}'`
            ].concat(allowedOrigins),
            childSrc: [self].concat(allowedOrigins),
            // If you have external apps, that should be allowed as sources for
            // connections or images, your should add them here
            // Call helmetOptions() without args if you have no external sources
            // Note, that this is just an example and you may configure this to your needs
            connectSrc: connectSrc.concat(allowedOrigins),
            fontSrc: [self, data].concat(allowedOrigins),
            formAction: [self],
            frameAncestors: [self],
            frameSrc: ['*'],
            // This is an example to show, that we can define to show images only
            // from our self, browser data/blob and a defined set of hosts.
            // Configure to your needs.
            imgSrc: [self, data, 'blob:'].concat(allowedOrigins),
            manifestSrc: [self].concat(allowedOrigins),
            mediaSrc: [self].concat(allowedOrigins),
            objectSrc: [self].concat(allowedOrigins),
            // these are just examples, configure to your needs, see
            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
            sandbox: [
                // allow-downloads-without-user-activation // experimental
                'allow-forms',
                'allow-modals',
                // 'allow-orientation-lock',
                // 'allow-pointer-lock',
                // 'allow-popups',
                // 'allow-popups-to-escape-sandbox',
                // 'allow-presentation',
                'allow-same-origin',
                'allow-scripts',
                // 'allow-storage-access-by-user-activation ', // experimental
                // 'allow-top-navigation',
                // 'allow-top-navigation-by-user-activation'
            ],
            styleSrc: [self, unsafeInline].concat(allowedOrigins),
            workerSrc: [self, 'blob:'].concat(allowedOrigins)
        }
    },
    // see the helmet documentation to get a better understanding of
    // the following configurations and settings
    strictTransportSecurity: {
        maxAge: 15552000,
        includeSubDomains: true,
        preload: false
    },
    referrerPolicy: {
        policy: 'no-referrer'
    },
    expectCt: {
        enforce: true,
        maxAge: 604800
    },
    frameguard: {
        action: 'sameorigin'
    },
    dnsPrefetchControl: {
        allow: false
    },
    permittedCrossDomainPolicies: {
        permittedPolicies: 'none'
    }
}

// We assume, that we are working on a localhost when there is no https
// connection available.
// Run your project with --production flag to simulate script-src hashing
if (!usesHttps && Meteor.isDevelopment) {
    delete helmetOptions.contentSecurityPolicy.directives.blockAllMixedContent
    helmetOptions.contentSecurityPolicy.directives.scriptSrc = [self, unsafeEval, unsafeInline].concat(allowedOrigins)
}

WebApp.connectHandlers.use(helmet(helmetOptions))

as I wrote previously…

Thanks @peterfkruger. I’ve edited the above post – so far it seems like I got it working. :grinning:

Happy to hear that! :wink:

1 Like

Hmmm… after doing an empty cache and hard reload, I’m still getting one of these:

Access to font at ‘https://cdn.userway.org/widgetapp/bundles/udf/UserwayDyslexiaFont-Italic.woff’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource

If anyone sees what I’m missing, please post. :grinning:

That stuff is way too complicated for my needs, I don’t even understand what is going on in that example. This is what I have in my app, mostly reduced to the font issue that does not seem to work in your case:

Meteor.startup(() => {
  const self = "'self'";
  const cloudCDN = "https://cdn.myapp.com/";
  const fontsGStaticUrl = "https://fonts.gstatic.com/";
  const fontsGoogleApisUrl = "https://fonts.googleapis.com/css";

  WebApp.connectHandlers.use(
    helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: [ self, cloudCDN ],
        fontSrc: [ self, cloudCDN, fontsGStaticUrl ],
        styleSrc: [ self, cloudCDN, fontsGoogleApisUrl ],
      }
    })
  );
});

Hmmm… I didn’t change anything and this morning the error message isn’t showing up in the console, even after an empty cache and hard reload. :thinking:

I’m going to push to Galaxy and see if it works there yet. :slight_smile:

Results: I’m getting errors on Galaxy, and the error I was getting on localhost is back too. It seems to be intermittent for some reason.

Oh wait a minute. For this to work your CDN must respond with a header Access-Control-Allow-Origin: * for the directory where the font files are located.

See Access-Control-Allow-Origin - HTTP | MDN

Interesting! I just loaded my production site a bunch of times in the browser, deleting the cache etc., and so far the console log is clear of errors. Maybe it took a while for my Cloudfront CDN to catch up with my latest code. If the errors recur I will try out your tip by updating settings on my Cloudfront distribution.

Hmmm… if I’m reading this correctly:

…the error message I’m getting is coming from the server providing the font, and has nothing to do with any setup on my server or CDN.

Is that correct?

For easy reference, here’s the error I’m getting:

Access to font at ‘https://cdn.userway.org/widgetapp/bundles/udf/UserwayDyslexiaFont-Italic.woff’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource

The way I’m reading this is that your browser refuses to accept the font file from your CDN, because…

…meaning your CDN doesn’t set this header in the response.

That may be right. I’ve got a question up on on StackOverflow asking about how to set up AWS Cloudfront CDN for this, with jpegs showing Cloudfront options:

Courtesy of AWS tech support, the correct headers to set are:

  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Origin

Additional relevant info:

When AWS CDN propagation is complete, the status of your CDN distribution changes from InProgress to Deployed.

It’s sometimes a good idea to invalidate the CDN copy of your app so that all files are reloaded. To do this:

  1. Sign in to the AWS Management Console and open the CloudFront console at hbps://console.aws.amazon.com/cloudfront/.
  2. Select the distribution for which you want to invalidate files.
  3. Choose Distribution Settings.
  4. Choose the Invalidations tab.
  5. Choose Create Invalidation.
  6. For the files that you want to invalidate, enter *. This will force cloudfront to evict all files which are cached in edge locations.