Allowing S3 images with npm helmet - node.js

I'm using npm helmet to secure my express app, but I want to allow images from my S3 bucket to render. I get this error when I use helment Refused to load the image '<URL>' because it violates the following Content Security Policy directive: "img-src 'self' data:". which makes sense, I am violating the CSP that helmet implements.
The only thing I've been able to find in their docs is:
app.use(
helmet({
contentSecurityPolicy: false,
})
);
which does allow my images to render, but I still want the CSP that helmet provides. I just need to essentially whitelist my S3 link but I cant find anything in their docs on this topic

Maintainer of Helmet here.
Helmet sets several security-related headers by default, including one called Content-Security-Policy. Content Security Policy, or CSP, is effectively an allowlist of what your page is allowed to load and do.
You're seeing an error that indicates that your CSP doesn't allow images to be loaded from anywhere other than your domain ('self') or data URIs (data:). You could fix this with something like:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
"img-src": ["'self'", "s3.amazonaws.com"],
},
},
})
);
For more on CSP, check out this introduction on MDN.
This is something that I've been thinking about making clearer in Helmet's docs, so thanks for asking this question.

Related

Helmet middleware disable drop down from working and create an error in the console

Why when I add the helmet middleware to my app, the dropdown menus stops working and I get the following error in the console:
Refused to load the script 'https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/js/bootstrap.bundle.min.js' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
This is how I added helmet to my app:
const helmet = require('helmet');
...
app.use(helmet())
Helmet maintainer here.
This is happening because of something called Content Security Policy, which Helmet sets by default. To solve your problem, you will need to configure Helmet's CSP.
MDN has a good documentation about CSP which I would recommend reading for background. After that, take a look at Helmet's README to see how to configure its CSP component.
To give some help specific to this question, let's take a look at one error you're seeing:
Refused to load the script 'https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/js/bootstrap.bundle.min.js' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
This error is telling you that the script-src directive of your CSP does not allow JavaScript to load from cdn.jsdelivr.net, and so it was blocked.
There are several ways to fix this:
Update your CSP to allow JavaScript to load from this domain. You'd do something like this:
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
"script-src": ["'self'", "cdn.jsdelivr.net"],
},
},
})
);
Refactor your app to avoid loading JavaScript from that domain. Probably not possible, but it is an available solution.
Disable CSP. This is the most dangerous option so I don't recommend it.
app.use(
helmet({
contentSecurityPolicy: false,
})
);
In summary: to solve your problem, you will need to tell Helmet to configure your CSP.

Ressources not loaded after setting CSP and CORP headers using Helmet

Trying to make the web-app safer and force myself to control better future additions (JS and CSS assets on different CDNs), I'm running Helmet plugin in my Fastify (same as Express) web-app.
If I deactivate all Helmet controls like the following:
fastify.register(helmet, false) all works fine and all resources are loaded on client.
Then I tried to play (until exhaustion) with different configurations, nothing is working. The config and the browser error as the following:
{
// contentSecurityPolicy: false,
crossOriginResourcePolicy: { policy: 'same-site'},
contentSecurityPolicy: {
directives: {
...require("fastify-helmet").contentSecurityPolicy.getDefaultDirectives(),
"default-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'", 'unpkg.com', 'cdn.jsdelivr.net',
'fonts.googleapis.com', 'use.fontawesome.com'],
"script-src": ["'self'", 'unpkg.com', "cdn.jsdelivr.net", "'unsafe-inline'"],
"img-src": ["'self'", "'data'", "*.tile.osm.org"],
"font-src": ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com', 'use.fontawesome.com']
},
},
};
Even setting
{ contentSecurityPolicy: false, crossOriginResourcePolicy: { policy: 'same-site'} }
with other variations of policy: same-origin, cross-origin none seems to work.
As you can see, I'm running on LOCALHOST too and I didn't test elsewhere.
tl;dr: disable the Cross-Origin-Embedder-Policy header, enabled by default in Helmet v5.
{
crossOriginEmbedderPolicy: false,
// ...
}
Helmet maintainer here.
Helmet sets the the Cross-Origin-Embedder-Policy HTTP response header to require-corp.
Setting this header means that loading cross-origin resources (like an image from another resource) is trickier. For example, loading a cross-origin like this...
<img alt="My picture" src="https://example.com/image.png">
...won't work unless example.com explicitly allows it, by setting some response headers of its own. Your browser will try to load example.com/image.png, and if it's not explicitly allowed, your browser will drop the response.
To fix this, you can prevent Helmet from setting the Cross-Origin-Embedder-Policy header, like this:
app.use(
helmet({
crossOriginEmbedderPolicy: false,
// ...
})
);
I made a small sample app you can use to play around with this.

How to prevent ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep?

I am attempting to access my movie API that returns data including an image of a movie poster through a React application. This image is being requested from an external website. Each time I make a request to my \movies endpoint, the image is blocked and I get the following message in the console
net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep 200
When looking at the request in the Network tab, I get the following message saying to enable a Cross-Origin Resource Policy
Because your site has the Cross-Origin Embedder Policy (COEP) enabled, each resource must specify a suitable Cross-Origin Resource Policy (CORP). This behavior prevents a document from loading cross-origin resources which don’t explicitly grant permission to be loaded.
To solve this, add the following to the resource’s response header:
Cross-Origin-Resource-Policy: same-site if the resource and your site are served from the same site.
Cross-Origin-Resource-Policy: cross-origin if the resource is served from another location than your website. ⚠️If you set this header, any website can embed this resource.
I am using the CORS npm module which had previously been used to solve my issue with an Access-Control-Allow-Origin error. I added some additional middleware to try and add the header as instructed. This is the app.js server with that code
App.js
'use strict';
import express, { json, urlencoded } from 'express';
import morgan from 'morgan';
import mongoose from 'mongoose';
import passport from 'passport';
import cors from 'cors';
import dotenv from 'dotenv';
import auth from './routes/auth.js';
import routes from './routes/routes.js';
dotenv.config();
const app = express();
mongoose
.connect(process.env.CONNECTION_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(res => console.log('DB Connected!'))
.catch(err => console.log(err, err.message));
app.use(cors())
app.use((req, res, next) => {
res.header("Cross-Origin-Resource-Policy", "cross-origin")
next()
})
app.use(passport.initialize());
app.use(json());
app.use(urlencoded({ extended: true }));
app.use(express.static(`public`));
app.use(morgan('common'));
auth(app);
import './authentication/passport.js';
routes(app)
app.use((req, res, err, next) => {
if (err) {
console.error(err.stack);
res.status(500).send('Something broke!');
}
next();
});
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => console.log(`Listening on Port ${port}`));
After doing this, the console throws the same error and the Cross-Origin Resource Policy still is not set. Is there something wrong with my approach or the way that I have my file structured?
You have COEP enabled in the client:
Cross-Origin-Embedder-Policy: require-corp
This is a great security feature that means:
COEP: Everything (data, images etc) on this website is mine, or I fetch from it from other websites using CORS. (There can be a third way, that is data being authorized by cookies, http-auth, etc... which is not in our discussion, so don't bother here.)
So, you have two options. The first one is to disable COEP, but I assume that you don't want to do that. So, the other option is to use CORS for everything external. For example, when you fetch something, use:
fetch('https://externalwebsite.com/image.jpg',{mode:'cors'})
or, to embed an external image in the HTML, use crossorigin
<img crossorigin="anonymous" src="https://externalwebsite.com/image.jpg">
Note that crossorigin attribute in <img> means CORS. If it is missing, it means "no-cors", which is the default. Be aware though: When you use JavaScript's fetch, the default is {mode:'cors'}, i.e. the opposite!
Now, if you try to do that (use CORS, as you should), the browser will throw another error:
Access [...] has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
That means... exactly that! That the external server has to send the header:
Access-Control-Allow-Origin: *
That setting means that every website can use the server's resources (API in your case), as long as it does not use/send/receive cookies in the request (because... security). The way to implement this in your express server is to set:
res.header('Access-Control-Allow-Origin', '*');
Every server that intends to serve things to other websites, must have this ACAO header. (You can place your other website instead of "*" if you want only that website to access your API.)
Note/Summary:
If the external server has this ACAO header, you can fetch things using CORS/crossorigin. If it does not have ACAO header, you can fetch things with no-cors / without crossorigin. But with COEP enabled in your website, you can only fetch with CORS/crossorigin, so the external server has to have an ACAO.
Now,
As for the Cross-Origin-Resource-Policy that your server has, have in mind that (https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)):
The policy is only effective for no-cors requests
During a cross-origin resource policy check, if the header is set, the browser will deny no-cors requests issued from a different origin/site.
This means that, since you make only CORS requests to that server, this header doesn't do anything (in your case). So the server can set it to "same-site"/"same-origin" for security reasons that are beyond this topic.

How to configure Content-Policy-Security header on ExpressJs app?

I'm want to create a link in my website who download a PDF file but I get a white window with:
Cannot GET /file
and a Content Security Policy (CSP) error:
Content Security Policy: The page settings prevented a resource from loading at inline ("default-src").
I use ExpressJs and Nginx for the back-end.
I've tried to set CSP default-src header like this in my app.js file:
//Headers setup
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',"default-src 'self' https://www.mydomain.fr");
next();
});
i've also tried to add this in my .conf file in Nginx:
add_header Content-Security-Policy "default-src 'self';" always;
It is still not working. Do you have any idea what is wrong?
My router:
const express = require('express');
const router = express.Router();
const fileController = require('../controllers/file');
router.get('/', fileController.getFile);
module.exports = router;
My controller:
const path = require('path');
exports.getFile = (req, res, next) => {
res.set({'Content-Type':'application/pdf'});
res.set({'Content-Disposition':'attachment, filename=file.pdf'});
const filePath = path.join(__dirname, '/public/documents/file.pdf');
res.download(filePath, 'file.pdf', (e) => {res.status(404).json({e: e})});
}
I guess your resource is embedded inline (see the csp error).
To allow that use unsafe-inline:
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',"default-src 'self' 'unsafe-inline'");
next();
});
SabSab43, you have been given the correct leads in the comments and answers, but you shouldn't recklessly publish the CSP. You can publish CSP in several ways:
add_header in Nginx
HTTP header via res.setHeader()
specialized Helmet package
using <meta http-equiv='Content-Security-Policy' content="...CSP rules here..."> in the HTML code.
But if you do use all of above at the same time, you'll publish a several different CSPs and as result more restrictive one will acts.
Therefore remove all CSP you added and lets go step by step:
Do check the jonrsharpe's remark abt Helmet. The fact is that if you have Helmet 4 connected to Express, it issues the restrictive default-src 'self' policy by default.
Therefore after removing all your CSPs, check do you have CSP header in browser, tutorial is here. If CSP header is presents - some middleware (like Helmet) does publish it. You need to find it and modify the policy in it, or switch it off and use your res.setHeader(...)
If there is no header (and there is no meta tag either), publish the CSP with any of the methods, and add 'unsafe-inline' to it, as kmgt said. You need to have policy:
"default-src 'self' 'unsafe-inline' https://www.mydomain.fr"
to get rid of "The page settings prevented a resource from loading at inline ("default-src")"
Note that 'prevented a resource from loading at inline' can be about not only inline <script>, but about inline <style> too. But since you use `default-src', it covers both.
PS: Chrome's console more verbose than Firefox's one. If you fail to fix this issue, pls show the message about the blocking from the Сhrome's console - it will show the rules that block.
I've deleted all my CSP Rules (I don't use Helmet or any CSP dependencies) in my app and Nginx .conf file and when I look in browsers developers tools I see again the CSP Rule.
In chrome:
capture
In Firefox:
capture
therefore, I've tried to put this code:
app.use((req, res, next) => {
res.set({"Content-Security-Policy":"default-src 'self' 'unsafe-inline' https://www.myDomain.fr"});
next();
});
I've the same result than without the CSP Rules...
In Chrome I juste have a 404 error:
GET https://www.myDomain.fr/file 404 (Not found)
In Firefox
GET https://www.myDomain.fr/file [HTTP/1.1 404 Not found 96ms]
Content Security Policy: The page settings prevented a resource from loading at inline ("default-src").
The 404 is probably caused by the CSP Error...

How to use helmet?

I want to use helmet package to do the following:
set X-Frame-Options = SAMEORIGIN
Disable the X-Powered-By header.
What should Content-Security-Policy be and how do I set it using helmet?
How about Access-Control-Allow-Origin?
I also want to use it to enable best practices for security. What do you suggest? What are these best practices and how do I set them?
Maintainer of Helmet here.
First of all, Helmet is not enough to make your Express apps secure. That requires understanding best practices, vulnerabilities, and much more. Helmet only tries to tackle a narrow piece of that puzzle: setting various HTTP response headers related to security.
For example, by default, Helmet will set a header called X-Frame-Options to SAMEORIGIN. This header doesn't magically make your app secure, but it can help mitigate clickjacking attacks. It will also disable a header called X-Powered-By by default, which
Here's how you use Helmet with all of its default settings:
app.use(helmet());
If you want to, say, override the default value for X-Frame-Options, you could do something like this:
// Sets all of the defaults except for X-Frame-Options,
// which is set to "DENY" instead of its default
app.use(helmet({
frameguard: { action: 'DENY' },
}));
And if you want Helmet to ignore the X-Frame-Options header completely:
// Sets all of the defaults except for X-Frame-Options
app.use(helmet({
frameguard: false,
}));
By default, Helmet is responsible for 11 headers, including the two mentioned above.
Helmet's most important, and most difficult to configure, header is Content-Security-Policy. It's not worth describing in depth here; I recommend reading MDN's introductory article.
Helmet can help you set the Content-Security-Policy header, which you can read more about on Helmet's docs. Here's a simple example:
app.use(
helmet.contentSecurityPolicy({
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
"script-src": ["'self'", "example.com"],
},
})
);
You also asked about Access-Control-Allow-Origin. This is part of something called Cross-Origin Resource Sharing, which Helmet does not touch.
You can write something like this:
app.use(helmet({
frameguard: false // for SAMEORIGIN
}));
app.disable('x-powered-by'); // for disable the X-Powered-By header.

Resources