White list Cloudflare on Heroku

#1

Hi, I’ve setup my Cloudflare to cache my site and that’s great.

I’d like to secure my backend on Heroku to only talk to Cloudflare.

I used this npm module with this IP list and it didn’t work.

Note that it’s checking Express’s req.ip for the IP.

From testing, it appears the requests are coming from different IP’s through Heroku - roughly:

Apr 15 09:52:32 action-be app/web.1: ::ffff:10.5.154.92 
Apr 15 09:52:32 action-be app/web.1: ::ffff:10.7.158.132 
Apr 15 09:52:32 action-be app/web.1: ::ffff:10.5.154.92 
Apr 15 09:52:34 action-be app/web.1: ::ffff:10.5.154.92 
Apr 15 09:52:34 action-be app/web.1: ::ffff:10.45.6.98 
Apr 15 09:52:34 action-be app/web.1: ::ffff:10.45.6.98 
Apr 15 09:52:34 action-be app/web.1: ::ffff:10.5.162.108 

req.ips is empty on Heroku.

The headers I have to work with are:

{ host: 'xxxxx', 
   connection: 'close', 
   'accept-encoding': 'gzip', 
   'cf-ipcountry': 'US', 
   'x-forwarded-for': '2601:1c2:5100:e867:453c:6b88:494e:d061, 172.68.174.94', 
   'cf-ray': 'xxxxxxxxx-xxx', 
   'x-forwarded-proto': 'https', 
   'cf-visitor': '{"scheme":"https"}', 
   'cache-control': 'max-age=0', 
   'upgrade-insecure-requests': '1', 
   'user-agent': 'xxxxxxx', 
   accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 
   'accept-language': 'en-US,en;q=0.9', 
   cookie: '__cfduid=xxxxx', 
   'cf-connecting-ip': '2601:1c2:5100:e867:453c:6b88:494e:d061', 
   'cdn-loop': 'cloudflare', 
   'x-request-id': 'xxxxxx', 
   'x-forwarded-port': '443', 
   via: '1.1 vegur', 
   'connect-time': '0', 
   'x-request-start': 'xxxxxx', 
   'total-route-time': '0' } 

I don’t think I can trust “cf-connecting-ip” because anyone could set it.

I think the “x-forwarded-for” headers are what I want. Heroku Docs.

Can “x-forwarded-for” be trusted in this context?
browser -> CloudFlare -> Heroku -> server

If “x-forwarded-for” is a Cloudflare IP, then I think I can trust the other “cf-xxx” headers.

Do you see any flaws with that logic?

Thank you!

Michael

0 Likes

#2

If you’re going to go with x-forwarded-for, you probably mean to check the IP address against the Cloudflare list. That seems to be a lot of work for something that doesn’t happen often. cf-connecting-ip is more unique to Cloudflare.

Generally I just test for the existence of one Cloudflare header and reject if it’s not there. That’s worked well for me, but you may want to check for multiple headers if you want to be more sure.

0 Likes

#3

This really is an odd situation, it would be nice if Heroku had a X-Heroku-Ip or similar header like Cloudflare that couldn’t be changed by the client

This SO post claims that the real client IP (in this case Cloudflare) should be the last IP in X-Forwarded-For, which seems to be consistent with your request (172.68.174.94 is Cloudflare). You should not trust it if the IP is just in the header since a malicious actor could send a fake Cloudflare IP, so it needs to be the last entry. If it all checks out, you should be able to safely use the Cf-Connecting-Ip.

The only concern with this is that the Heroku Docs are not clear about this behavior, so it may (hopefully not) be changed in the future.

0 Likes

#4

Hi sdayman, thanks for the reply :slight_smile:

For me, the missing piece above was app.enable('trust proxy'); That configures Express to populate the req.ips array.

Below is the code I ended up using. Trusting cf-connecting-ip I think is worse because someone could fake it and take advantage of your assumptions it’s secure. That might work for automated scans, but not focused attention.

Anyways, this is the code I started with:

// Should we trust x-forwarded-for headers?
if (app.get('trustProxy')) app.enable('trust proxy');

// Whitelist IP's
app.use(function(req, res, next) {
  let ips = req.ips || [];
  ips.push(req.ip);
  for (let x = 0; x < ips.length; x++) {
    // Is on white list?
    if (ipRangeCheck(ips[x], app.get('IPwhitelist'))) return next();
  }
  // No IPS on white list
  res.redirect(307, app.get('redirect'));
});
1 Like