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
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,161 @@ describe('middlewares', () => {
});
});
});

it('should not succeed if the ip does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.ip = 'ip3';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should succeed if the ip does belong to masterKeyIps list', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.ip = 'ip1';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.connection = {remoteAddress : 'ip3'};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.connection = {remoteAddress : 'ip1'};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.socket = {remoteAddress : 'ip3'};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.socket = {remoteAddress : 'ip1'};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.connection = { socket : {remoteAddress : 'ip3'}};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1','ip2']
});
fakeReq.connection = { socket : {remoteAddress : 'ip1'}};
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should allow any ip to use masterKey if masterKeyIps is empty', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: []
});
fakeReq.ip = 'ip1';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should succeed if xff header does belong to masterKeyIps', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1']
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should succeed if xff header with one ip does belong to masterKeyIps', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1']
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1';
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});

it('should not succeed if xff header does not belong to masterKeyIps', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip4']
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should not succeed if xff header is empty and masterKeyIps is set', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1']
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = '';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
});
14 changes: 14 additions & 0 deletions spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,18 @@ describe('server', () => {
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' })
.catch(done);
});

it('fails if you provides invalid ip in masterKeyIps', done => {
reconfigureServer({ masterKeyIps: ['invalidIp','1.2.3.4'] })
.catch(error => {
expect(error).toEqual('Invalid ip in masterKeyIps: invalidIp');
done();
})
});

it('should suceed if you provide valid ip in masterKeyIps', done => {
reconfigureServer({ masterKeyIps: ['1.2.3.4','2001:0db8:0000:0042:0000:8a2e:0370:7334'] })
.then(done)
});

});
15 changes: 14 additions & 1 deletion src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import AppCache from './cache';
import SchemaCache from './Controllers/SchemaCache';
import DatabaseController from './Controllers/DatabaseController';
import net from 'net';

function removeTrailingSlash(str) {
if (!str) {
Expand All @@ -26,6 +27,7 @@ export class Config {
this.applicationId = applicationId;
this.jsonLogs = cacheInfo.jsonLogs;
this.masterKey = cacheInfo.masterKey;
this.masterKeyIps = cacheInfo.masterKeyIps;
this.clientKey = cacheInfo.clientKey;
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
Expand Down Expand Up @@ -86,7 +88,8 @@ export class Config {
sessionLength,
emailVerifyTokenValidityDuration,
accountLockout,
passwordPolicy
passwordPolicy,
masterKeyIps
}) {
const emailAdapter = userController.adapter;
if (verifyUserEmails) {
Expand All @@ -108,6 +111,8 @@ export class Config {
}

this.validateSessionConfiguration(sessionLength, expireInactiveSessions);

this.validateMasterKeyIps(masterKeyIps);
}

static validateAccountLockoutPolicy(accountLockout) {
Expand Down Expand Up @@ -184,6 +189,14 @@ export class Config {
}
}

static validateMasterKeyIps(masterKeyIps) {
for (const ip of masterKeyIps) {
if(!net.isIP(ip)){
throw `Invalid ip in masterKeyIps: ${ip}`;
}
}
}

get mount() {
var mount = this._mount;
if (this.publicServerURL) {
Expand Down
7 changes: 7 additions & 0 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ParseServer {
constructor({
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
masterKeyIps = [],
appName,
analyticsAdapter,
filesAdapter,
Expand Down Expand Up @@ -167,6 +168,11 @@ class ParseServer {
userSensitiveFields
)));

masterKeyIps = Array.from(new Set(masterKeyIps.concat(
defaults.masterKeyIps,
masterKeyIps
)));

const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, { jsonLogs, logsFolder, verbose, logLevel, silent });
const loggerController = new LoggerController(loggerControllerAdapter, appId);
logging.setLogger(loggerController);
Expand Down Expand Up @@ -228,6 +234,7 @@ class ParseServer {
AppCache.put(appId, {
appId,
masterKey: masterKey,
masterKeyIps:masterKeyIps,
serverURL: serverURL,
collectionPrefix: collectionPrefix,
clientKey: clientKey,
Expand Down
5 changes: 5 additions & 0 deletions src/cli/definitions/parse-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export default {
help: "Your Parse Master Key",
required: true
},
"masterKeyIps": {
env: "PARSE_SERVER_MASTER_KEY_IPS",
help: "Restrict masterKey to be used by only these ips. defaults to [] (allow all ips)",
default: []
},
"port": {
env: "PORT",
help: "The port to run the ParseServer. defaults to 1337.",
Expand Down
3 changes: 2 additions & 1 deletion src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ export default {
cacheTTL: 5000,
cacheMaxSize: 10000,
userSensitiveFields: ['email'],
objectIdSize: 10
objectIdSize: 10,
masterKeyIps: []
}
9 changes: 9 additions & 0 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

(req.connection && req.connection.remoteAddress) ||
(req.socket && req.socket.remoteAddress) ||
(req.connection && req.connection.socket && req.connection.socket.remoteAddress) ||
req.ip;
if (info.masterKey && req.config.masterKeyIps && req.config.masterKeyIps.length !== 0 && req.config.masterKeyIps.indexOf(ip) === -1) {
return invalidRequest(req, res);
}

var isMaster = (info.masterKey === req.config.masterKey);

if (isMaster) {
Expand Down