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:
committed by
Florent Vilmart
parent
811d8b0c7a
commit
7e54265f6d
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -35,5 +35,6 @@ export default {
|
||||
cacheTTL: 5000,
|
||||
cacheMaxSize: 10000,
|
||||
userSensitiveFields: ['email'],
|
||||
objectIdSize: 10
|
||||
objectIdSize: 10,
|
||||
masterKeyIps: []
|
||||
}
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
Reference in New Issue
Block a user