Skip to content

Security: limit Masterkey remote access #4017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

pungme
Copy link
Contributor

@pungme pungme commented Jul 14, 2017

  • add config option to specify list of ip addresses to allow masterKey to be used
  • This is good for doubling down the security, even if your masterKey is leaked, it still getting blocked based on ip address.
  • feel free to suggest! :-)
  • Closes Security: limit Masterkey remote access #1987

@flovilmart
Copy link
Contributor

Hey Thanks for the PR, can you add the option also to the CLI options?

@pungme
Copy link
Contributor Author

pungme commented Jul 14, 2017

Hey @flovilmart thanks for a prompt feedback! I added CLI options as you suggested. let me know if there's anything else i should do.

@codecov
Copy link

codecov bot commented Jul 14, 2017

Codecov Report

Merging #4017 into master will increase coverage by 0.02%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4017      +/-   ##
==========================================
+ Coverage    90.7%   90.73%   +0.02%     
==========================================
  Files         116      116              
  Lines        7889     7910      +21     
==========================================
+ Hits         7156     7177      +21     
  Misses        733      733
Impacted Files Coverage Δ
src/cli/definitions/parse-server.js 50% <ø> (ø) ⬆️
src/defaults.js 90% <ø> (ø) ⬆️
src/ParseServer.js 88.67% <100%> (+0.07%) ⬆️
src/middlewares.js 97.48% <100%> (+0.22%) ⬆️
src/Config.js 95.17% <100%> (+0.24%) ⬆️
...dapters/Storage/Postgres/PostgresStorageAdapter.js 96.37% <0%> (-0.13%) ⬇️
src/triggers.js 90.9% <0%> (+0.5%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e6cc820...358c136. Read the comment docs.

@montymxb
Copy link
Contributor

@pungme this is fantastic! Having the capacity to restrict requests, even with an authorized master key, definitely important from a security standpoint.

@@ -110,6 +110,11 @@ export function handleParseHeaders(req, res, next) {
req.config = new Config(info.appId, mount);
req.info = info;

const ip = req.ip;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hum, how does it work behind a LB? req.ip may be the LB's IP and not the original client IP right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Through a reverse proxy, yeah the IP would probably be that of the balancer :/. Although in some cases the requesting IP would be passed along in the X-Forwarded-For header, not necessarily guaranteed however. To note this could be more than one IP, depending on how many proxies it passed through.

Perhaps we could check for that header and use it's value instead? When present that is.

Copy link
Contributor

@montymxb montymxb Jul 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, express has a guide for running behind proxies. Allowing it to handle itself internally, req.ip would just return what it can deduce as the original client ip. All that would have to be done is to add the following to the express initialization code.

app.enable('trust proxy');

Copy link
Contributor Author

@pungme pungme Jul 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question. let me rework a bit and test with our loadbalancer environment

Copy link
Contributor Author

@pungme pungme Jul 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @flovilmart , I think @montymxb was right. We can handle the LB case with something like:

const ip = req.headers['x-forwarded-for'] ||
    req.connection.remoteAddress || ... etc

But the question is, should we try to handle that on parse-server side or let user configure that by using app.enable('trust proxy'); in the beginning?

Let me know what you think.

@benishak
Copy link
Contributor

a better security mechanism is to use a proxy and restrict CORS (Allow Origin)
https://stackoverflow.com/questions/3862813/how-can-i-use-an-http-proxy-with-node-js-http-client

@flovilmart
Copy link
Contributor

CORS is only implemented in web browsers, and you may wanna restrict by Ip nonetheless. It isn’t better just different

@montymxb
Copy link
Contributor

There is actually an npm cors package for connect/express, but cross origin requests, in the context of Access-Control-Allow-Origin as mentioned above, would restrict access to resources rather than filter actions performed on resources. We wouldn't to block someone from making a query, we simply want to prevent them from querying beyond their limitations.

The exception being CORS does feature Access-Control-Request-Headers, which allows you to filter out requests containing certain headers (like that for carrying the masterKey) to those who are specifically authorized to do so.

Still, I prefer what's being put together here, rather than to include another dependency.

@benishak
Copy link
Contributor

yes I do actually like the idea, we only need to make sure that the IP is passed correctly when using a reverse proxy

@benishak
Copy link
Contributor

maybe it is not big issue, most reserve proxy nginx, haproxy or even AWS ELB will set XFF header if configured correctly, we only need to improve the help message and let the user decide whether to use this feature or not, at the end it is not harmful ..

@flovilmart
Copy link
Contributor

The trust proxy option can always be added we should be handling for the ‘worst’ scenario :)

@miguel-s
Copy link
Contributor

@benishak as already mentioned, using CORS is a different issue that only really affects browsers. By generally restricting the ip we can make sure that, in case the master key leaks, no malicious requests get to the server, independently of origin (e.g. a node script).

@pungme it's probably better to not rely on app.enable('trust proxy'); and check for all sources of ip, like the x-forwarded-for header

@pungme
Copy link
Contributor Author

pungme commented Jul 17, 2017

Hey @flovilmart just updated the pull request based on your suggestions. Let me know what else I should work on. Thanks!

@benishak
Copy link
Contributor

@miguel-s yes I know forget about CORS for now, I'm talking about this pull request is not harmful, we only need to improve the help message and tell the user if you want to use the masterKeyIps they need to make sure that their reverse proxy nginx, haproxy or elb is configured correctly if they are using one. If the reverse proxy is correctly configured the header x-forwarded-for will be set correctly.

pungme : just add a notice to the help message to the user that if they are using reverse proxy they need to make sure it is configured to set the x-forwarded-for header, maybe adding a small chapter in the docs about it would be also good

https://easyengine.io/tutorials/nginx/forwarding-visitors-real-ip/

@@ -110,7 +110,11 @@ export function handleParseHeaders(req, res, next) {
req.config = new Config(info.appId, mount);
req.info = info;

const ip = req.ip;
const ip = req.headers['x-forwarded-for'] ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick thing, it is possible that this value may be multiple ips. Especially if the user passed through one or more proxies before getting to a balancer, (quick breakdown here).

Considering that if you have more than 1 IP, and they are each separated by a ,, you can just split the string apart by it's commas to and take the first 'origin' entry (or the only entry).

req.headers['x-forwarded-for'].split(',')[0]

@montymxb
Copy link
Contributor

@pungme can you add some master key allowed/denied tests accounting for the x-forwarded-for header being set? Additionally can you also factor for multiple ips? I left a comment in the new code mentioning that a bit more.

@nbering
Copy link

nbering commented Jul 17, 2017

Could this support a subnet range instead of a specific IP? Right now I have some code that runs in AWS Lambda to perform tasks against Parse Server using the rest API and the master key. It's part of the same private network, but would never come from a guaranteed specific IP address.

@pungme
Copy link
Contributor Author

pungme commented Jul 17, 2017

Hey @montymxb, thank you so much for your comment :-) I've updated the change as you suggested. Let me know if there are anything else I should change.

@pungme
Copy link
Contributor Author

pungme commented Jul 17, 2017

@nbering This could be the next step of this feature. it shouldn't be too hard to support that. But i'd prefer to getting this one done first :-)

@nbering
Copy link

nbering commented Jul 17, 2017

Validating that the value is in fact an IP address would probably help to shim in subnet-based stuff later without breaking people - because a good value was enforced up-front.

There's IP address validation built into NodeJS core on the net module.

https://nodejs.org/dist/latest-v6.x/docs/api/net.html#net_net_isip_input

@montymxb
Copy link
Contributor

@nbering I like the idea of being able to match up to a subnets, but I think for step one let's go with what we have here. Assuming this plays nice once merged, and we don't have any issues regarding forwarded-for I think it would be an excellent idea to have subnet matching built in later.

@pungme no problem! I like the test cases 👍 . Can't think of too much else besides the aformentioned ip validation via net.isIP(ip), particularly for the ips listed for master key access. It could be good to let them know in case they drop in an invalid IP by chance, and additionally to add a test case trying to add a 'bad' ip (you have to correct your existing entries). If you can sanitize that we should be pretty close to being ready here.

@montymxb
Copy link
Contributor

Excellent! Well this looks pretty good to me. @flovilmart any thoughts or concerns on this as it stands?

@@ -110,6 +110,15 @@ export function handleParseHeaders(req, res, next) {
req.config = new Config(info.appId, mount);
req.info = info;

const ip = (req.headers['x-forwarded-for'] && req.headers['x-forwarded-for'].split(',')[0]) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe extract this into a function getClientIp and document each check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@flovilmart
Copy link
Contributor

One small nit for the ip extraction otherwise it’s great!

@pungme
Copy link
Contributor Author

pungme commented Jul 18, 2017

@flovilmart thanks a lot for your reply, I've changed as you suggested. Let me know if there are anything else 😊

@flovilmart
Copy link
Contributor

Let's see how it goes!

@flovilmart flovilmart merged commit 7e54265 into parse-community:master Jul 23, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants