Security: limit Masterkey remote access (#4017)

* update choose_password to have the confirmation

* add comment mark

* First version, no test

* throw error right away instead of just use masterKey false

* fix the logic

* move it up before the masterKey check

* adding some test

* typo

* remove the choose_password

* newline

* add cli options

* remove trailing space

* handle  in case the server is behind proxy

* add getting the first ip in the ip list of xff

* sanity check the ip in config if it is a valid ip address

* split ip extraction to another function

* trailing spaces
This commit is contained in:
Worathiti Manosroi
2017-07-23 18:26:30 +02:00
committed by Florent Vilmart
parent 811d8b0c7a
commit 7e54265f6d
7 changed files with 223 additions and 2 deletions

View File

@@ -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);
});
});

View File

@@ -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)
});
});

View File

@@ -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) {
@@ -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;
@@ -86,7 +88,8 @@ export class Config {
sessionLength,
emailVerifyTokenValidityDuration,
accountLockout,
passwordPolicy
passwordPolicy,
masterKeyIps
}) {
const emailAdapter = userController.adapter;
if (verifyUserEmails) {
@@ -108,6 +111,8 @@ export class Config {
}
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
this.validateMasterKeyIps(masterKeyIps);
}
static validateAccountLockoutPolicy(accountLockout) {
@@ -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) {

View File

@@ -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,
@@ -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);
@@ -228,6 +234,7 @@ class ParseServer {
AppCache.put(appId, {
appId,
masterKey: masterKey,
masterKeyIps:masterKeyIps,
serverURL: serverURL,
collectionPrefix: collectionPrefix,
clientKey: clientKey,

View File

@@ -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.",

View File

@@ -35,5 +35,6 @@ export default {
cacheTTL: 5000,
cacheMaxSize: 10000,
userSensitiveFields: ['email'],
objectIdSize: 10
objectIdSize: 10,
masterKeyIps: []
}

View File

@@ -111,6 +111,11 @@ export function handleParseHeaders(req, res, next) {
req.config.headers = req.headers || {};
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) {
@@ -171,6 +176,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 ;