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 all 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: []
}
24 changes: 24 additions & 0 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export function handleParseHeaders(req, res, next) {
req.config = new Config(info.appId, mount);
req.info = info;

const ip = getClientIp(req);
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 Expand Up @@ -170,6 +175,25 @@ export function handleParseHeaders(req, res, next) {
});
}

function getClientIp(req){
if (req.headers['x-forwarded-for']) {
// try to get from x-forwared-for if it set (behind reverse proxy)
return req.headers['x-forwarded-for'].split(',')[0];
} else if (req.connection && req.connection.remoteAddress) {
// no proxy, try getting from connection.remoteAddress
return req.connection.remoteAddress;
} else if (req.socket) {
// try to get it from req.socket
return req.socket.remoteAddress;
} else if (req.connection && req.connection.socket) {
// try to get it form the connection.socket
return req.connection.socket.remoteAddress;
} else {
// if non above, fallback.
return req.ip;
}
}

function httpAuth(req) {
if (!(req.req || req).headers.authorization)
return ;
Expand Down