diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 32de182e..7543f370 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -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); + }); }); diff --git a/spec/index.spec.js b/spec/index.spec.js index 3be95724..46e57e26 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -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) + }); + }); diff --git a/src/Config.js b/src/Config.js index 57955bb0..2678b6a0 100644 --- a/src/Config.js +++ b/src/Config.js @@ -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) { diff --git a/src/ParseServer.js b/src/ParseServer.js index 43f26f63..252c3cbd 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -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, diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 4e267787..63762239 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -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.", diff --git a/src/defaults.js b/src/defaults.js index f406626a..ef541c1f 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -35,5 +35,6 @@ export default { cacheTTL: 5000, cacheMaxSize: 10000, userSensitiveFields: ['email'], - objectIdSize: 10 + objectIdSize: 10, + masterKeyIps: [] } diff --git a/src/middlewares.js b/src/middlewares.js index 7e9e0920..11349a51 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -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 ;