How do I prevent people from hotlinking images on an app using nodejs and express? I'm hosting on my own server. Is this something I would need to build into the app.js file? Thanks.
You need to write middleware to only accept certain valid referers from your website. Be sure to accept modern search engines, social media sites, and no referers too.
If you are using nginx with node, here is a solution.
Basically, it checks for referrer and returns 403 if not authorized.
You could also write a middleware for express using the same logic.
Create a middleware to check Referer in HTTP request header, with domains of popular search engines, media, and websites.
Add this in your app.js file. Assuming you are using express 4.x.
app.use('/', (req , res , next) => {
const referrer = req.get('Referrer');
if (referrer == null) {
next(); // referrer is an optional http header, it may not exist
}
if (["https://www.facebook.com", "https://www.google.com"].includes(referrer)) { // compare referrer with your whitelist
res.status(403).send("You are not allowed to view this photo!"); // block the request
}
next();
});
app.get('/',(req,res)=>{
res.sendFile(`PATH_OF_YOUR_IMAGE`);
});
Find out more about Referer HTTP header
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
The issue using referrer is that most hotlinks do not use this header and browsers do not send it either with the image in an tag or src="" property... if your website is using cookies and cookies are accepted by the users of your website you could verify the presence of the cookie in the request by adding the following server.get() funcion before the static folder generic case (assuming you use an express server):
server.get(['/assets/img/**','/assets/images/**'], (req, res, next) => {
if(req.url == '/assets/images/logo*.*') {
next();
}
console.log('requested image' + req.url);
let maxage;
if (!isBot(req)) {
let cookieLang = req.cookies['my_cookie'];
if (!cookieLang) {
console.log('no cookie ');
res.status(403);
maxage = 5; // 5 seconds, in order not to damage your website visibility if viewer ends up visiting your site
res.setHeader('Cache-Control', 'public, max-age='+maxage);
res.setHeader('Expires', new Date(Date.now() + maxage * 1000).toUTCString());
res.sendFile(join(distFolder + '/assets/images/logo.png'));
} else {
console.log('withcookie ' + cookieLang);
res.set({'Cache-Control': 'max-age=31536000'});
res.sendFile(join(distFolder + req.url));
}
} else {
res.set({'Cache-Control': 'max-age=31536000'});
res.sendFile(join(distFolder + req.url));
}
});
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
in my case in order to help me benefit from hotlinks I return the logo of our company instead of the requested image :o) but you could return just nothing.
Also in this example there is a isBot() funcion where I check the user agent in order to let WhatsApp, Twitter and other bots get the required image for company visibility when adding a link in a post in those social networks.
Any idea of improvement welcomed !
Related
Use case
I have some custom form for the frontend, that needs a signed in user.
Therefore I use keystone.js' own "requireUser" middleware.
After the user has signed in, to access the site, the default behaviour is to show the admin UI at "/keystone" - however I want to redirect back to the previous frontend page, so the user can start filling in the form.
What works
In keystone.js, in the file with the exact same name, you can set
keystone.set('signin redirect', '/');
which redirects to the home page after the signin.
What I need
However, what I need is to set the url to redirect to dynamically, but when I do this, the function is never called:
keystone.set('signin redirect', function(req, res){
var url = req.session.returnTo || '/';
res.redirect(url);
});
The req.session.returnTo parameter is set in the "middleware.js" file previously mentioned, by taking the req.path property from the frontend page:
exports.requireUser = function (req, res, next) {
if (!req.user) {
req.flash('error', 'Please sign in to access this page.');
req.session.returnTo = req.path;
res.redirect('/keystone/signin');
} else {
next();
}
};
My temporary workaround
In keystone/admin/server/routes/signin.js I extend the redirect property of the locals object from
redirect: keystone.get('signin redirect'),
to
redirect: (typeof keystone.get('signin redirect') === 'string') ?
keystone.get('signin redirect') : keystone.get('signin redirect')(req, res),
I am aware this is not an ideal solution, but since I am still learning about keystone.js, do you know a better way of handling this?
Notice: the same redirect strategy works when setting the "signout redirect" key (to both a string or a function as second parameter) so I guess this must be something that was forgotten to implement?
Thanks
#hajn
Following #1489 issue on Keystone project, there is no ideal solution as of now. Documentation didn't provide the function params properly (I'm considering that as an issue). So, I've reopened the issue 3 days back.
Change routes/middleware.js replacing the method exports.requireUser as follows:
/**
Prevents people from accessing protected pages when they're not signed in
*/
exports.requireUser = function (req, res, next) {
if (!req.user) {
req.flash('error', 'Please sign in to access this page.');
if(req.url) {
res.redirect('/keystone/signin?from='+req.url);
} else {
res.redirect('/keystone/signin');
}
} else {
next();
}
};
From any URL pointing to /keystone/signin, include a from parameter in the query string, as so:
Log in
or
Log in
there I want to extract the domain name of incoming request using request module. I tried by looking at
request.headers,
request.headers.hostname
with no luck. Any help please?
I figured it out. Client domain is available at origin.
request.headers.origin
for ex:
if(request.headers.origin.indexOf('gowtham') > -1) {
// do something
}
Thanks!
You should use request.headers.host .
So you want the domain name of the client that is making the request?
Since Express only provides you with their IP-address, you have to reverse-lookup that IP-address to get the hostname. From there, you can extract the domain name.
In a middleware
const dns = require('dns');
...
app.use(function(req, res, next) {
dns.reverse(req.ip, function(err, hostnames) {
if (! err) {
console.log('domain name(s):', hostnames.map(function(hostname) {
return hostname.split('.').slice(-2).join('.');
}));
}
next();
});
});
However, very big disclaimer: making a DNS lookup for every request can have a severe performance impact.
I am developing a MEAN stack based web app, using Cloud9 and Heroku. I want all users to be forced to use HTTPS, and I found a nice way using Express middleware:
app.use(function(req,res,next) {
if(req.headers["x-forwarded-proto"] == "http") {
console.log("HTTP call detected, not allowed);
return res.redirect('https://' + req.hostname + req.path);
} else {
console.log("HTTPs call detected, allowed");
return next();
}
I hope this can work for all GET, POST, PUT, DELETE requests I receive, thinking that any call to HTTP should just be redirected to the identical, corresponding HTTPS request.
Now while this in itself seems to work, I am getting the CORS policy error
XMLHttpRequest cannot load
https://myappurlhere.herokuapp.com/app. No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://myappurlhere.herokuapp.com' is
therefore not allowed access. The response had HTTP status code 401.
Now I do understand the concept that this is the standard CORS policy that prevents cross-domain requests, but I am not exactly sure how I should approach this. I thought this would go as a same-domain-request, but apparently not.
I have seen that some people argue that a second server should run and catch HTTP traffic, redirecting, but I am only using one server. I don't understand why that couldn't work.
Any help greatly appreciated.
Regards
function requireHTTPS(req, res, next) {
if (!req.secure) {
return res.redirect('https://' + req.get('host') + req.url);
}
next();
}
app.use(requireHTTPS);
This worked for me.
I made HTTPS traffic forced now, but I don't think I solved the actual problem. What I am now using is this:
app.use(function (req, res, next) {
res.setHeader('Strict-Transport-Security', 'max-age=8640000; includeSubDomains');
if (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'] === "http") {
return res.redirect(301, 'https://' + req.host + req.url);
} else {
console.log("HTTPS call detected");
return next();
}
});
It seems that this effectively forces strict TLS, probably because of the header change. However, this also mean that the redirect never seems to happen - the test for "http" is always false. Hence, the problem with COTS redirect policy remains.
This is where I would really like some input - should I add some policies allowing cross domain requests? That sounds insecure, and why? I am on the same domain, I just want HTTPS.
Any input appreciated.
I'm having troubles with Express 4 and static files. After a successful login i redirect my web-app to '/home/userId' but then, i get 404 on all static files. This is my router:
router.get('/home/:userid', function(req, res, next) {
// TODO check if token is valid
User.findById(req.params.userid).exec(function(err, find) {
if (err) return next(err);
if (!find) {
return res.json(utils.createErrorJsonResponse(404, "User not found!"));
}
return res.render('home', {
title: 'Organizator',
user: find
});
})
});
I think is not usefull show you my jade file, the only important thing is that there are lots of imported scripts, but just for give an example, this is how i load a css file:
link(href='css/style.css', rel='stylesheet')
This is how i setup the static file
app.use(express.static(config.root + '/public',{ maxAge: 86400000 }));
Where 'config.root' is my is:
path.normalize(__dirname + '/..')
As i said before if i connect to the basic page, so in my case:
http://localhost:3000
all my static files are imported, but as soon as i redirect i got 404. So how can i fix it? For example. I have a style file named 'style.css'. In the basic page ('http://localhost:3000) from the console i can see:
Request URL:http://localhost:3000/css/style.css
Request Method:GET
Status Code:200 OK (from cache)
Then from 'http://localhost:3000/home/':
Request URL:http://localhost:3000/home/css/style.css
Request Method:GET
Status Code:404 Not Found
So the problem is the '/home', but how can i "remove" it from static files request? Thank you.
As explained well #Molda in comments, make sure your links start with /, otherwise the path will be relative to the actual route http://localhost:3000/home.
I'm working on an application which allows you to upload images, and create albums. Everything works fine accept that after an album is created the new album isn't shown in the client until the page is reloaded, and I can't figure out how to solve this.
Below is the route for creating an album. Is it somehow possible to use something else than res.redirect('back') in this case so that the page is reloaded after the route has finished?
Of course I could copy/paste the code from the route for loading the page in the first place, but that would be to much DRY. Maybe I can call the other route from within this route?
route:
app.post('/album', function(req, res){
var newAlbum = new albumModel.Album();
newAlbum.imageName = req.body.albumPicsName;
newAlbum.imageId = req.body.albumPicsId;
newAlbum.title = req.body.albumTitle;
newAlbum.save(function (err) {
if (err) {
console.log(err);
// do something
console.trace();
}
res.redirect('back');
});
});
'back' is an alias for req.get('Referrer') so if '/albums' is your referrer you might still experience issues with the browser returning a 304 (http not modified) http status or a browser cached page (common). If you experience that issue you can do a couple of things:
Send some cache clearing headers:
res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
res.redirect('back');
Or modify the url:
var url = require('url');
var u = url.parse(req.get('Referrer'), true, false);
u.query['v'] = +new Date(); // add versioning to bust cache
delete u.search;
res.redirect(url.format(u));
Of course if you know the url such as '/albums' you don't have to go though all that rigamarole to add some versioning - just append current timestamp to the url string.
The first way is cleaner and works better imo. I've seen cases when a page doesn't have a referrer even though I clearly came from another page due to caching.
You should be able to do res.redirect('/album') instead of back to force a full reload but get the same type of feedback.