diff --git a/package-lock.json b/package-lock.json index ed019eb92d..d8b22a2d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7717,6 +7717,14 @@ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, + "ip-range-check": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.2.0.tgz", + "integrity": "sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw==", + "requires": { + "ipaddr.js": "^1.0.1" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index 07901b4466..dccdfe64fb 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ ], "license": "BSD-3-Clause", "dependencies": { - "@graphql-yoga/node": "2.6.0", - "@graphql-tools/utils": "8.12.0", "@graphql-tools/merge": "8.3.6", "@graphql-tools/schema": "9.0.4", + "@graphql-tools/utils": "8.12.0", + "@graphql-yoga/node": "2.6.0", "@parse/fs-files-adapter": "1.2.2", "@parse/push-adapter": "4.1.2", "bcryptjs": "2.4.3", @@ -34,9 +34,10 @@ "follow-redirects": "1.15.2", "graphql": "16.6.0", "graphql-list-fields": "2.0.2", - "graphql-tag": "2.12.6", "graphql-relay": "0.10.0", + "graphql-tag": "2.12.6", "intersect": "1.0.1", + "ip-range-check": "0.2.0", "jsonwebtoken": "8.5.1", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", @@ -59,7 +60,6 @@ "ws": "8.9.0" }, "devDependencies": { - "graphql-tag": "2.12.6", "@actions/core": "1.9.1", "@apollo/client": "3.6.1", "@babel/cli": "7.10.0", @@ -86,6 +86,7 @@ "eslint-plugin-flowtype": "5.1.3", "flow-bin": "0.119.1", "form-data": "3.0.0", + "graphql-tag": "2.12.6", "husky": "4.3.8", "jasmine": "3.5.0", "jasmine-spec-reporter": "7.0.0", diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 2c380152ba..5e1ec24d89 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -3,7 +3,6 @@ const AppCache = require('../lib/cache').AppCache; describe('middlewares', () => { let fakeReq, fakeRes; - beforeEach(() => { fakeReq = { originalUrl: 'http://example.com/parse/', @@ -117,10 +116,12 @@ describe('middlewares', () => { const otherKeys = BodyKeys.filter( otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey' ); - it(`it should pull ${bodyKey} into req.info`, done => { + AppCache.put(fakeReq.body._ApplicationId, { + masterKeyIps: ['0.0.0.0/0'], + }); + fakeReq.ip = '127.0.0.1'; fakeReq.body[bodyKey] = keyValue; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeReq.body[bodyKey]).toEqual(undefined); expect(fakeReq.info[infoKey]).toEqual(keyValue); @@ -134,161 +135,147 @@ describe('middlewares', () => { }); }); - it('should not succeed if the ip does not belong to masterKeyIps list', () => { + it('should not succeed if the ip does not belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1'], }); - fakeReq.ip = 'ip3'; + fakeReq.ip = '127.0.0.1'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); - it('should succeed if the ip does belong to masterKeyIps list', done => { + it('should succeed if the ip does belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1'], }); - fakeReq.ip = 'ip1'; + fakeReq.ip = '10.0.0.1'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => { + it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); - fakeReq.connection = { remoteAddress: 'ip3' }; + fakeReq.connection = { remoteAddress: '127.0.0.1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); - it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => { + it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); - fakeReq.connection = { remoteAddress: 'ip1' }; + fakeReq.connection = { remoteAddress: '10.0.0.1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => { + it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); - fakeReq.socket = { remoteAddress: 'ip3' }; + fakeReq.socket = { remoteAddress: '127.0.0.1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); - it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => { + it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); - fakeReq.socket = { remoteAddress: 'ip1' }; + fakeReq.socket = { remoteAddress: '10.0.0.1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => { + it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); fakeReq.connection = { socket: { remoteAddress: 'ip3' } }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); - it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => { + it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1', 'ip2'], + masterKeyIps: ['10.0.0.1', '10.0.0.2'], }); - fakeReq.connection = { socket: { remoteAddress: 'ip1' } }; + fakeReq.connection = { socket: { remoteAddress: '10.0.0.1' } }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should allow any ip to use masterKey if masterKeyIps is empty', done => { + it('should allow any ip to use masterKey if masterKeyIps is empty', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: [], + masterKeyIps: ['0.0.0.0/0'], }); - fakeReq.ip = 'ip1'; + fakeReq.ip = '10.0.0.1'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should succeed if xff header does belong to masterKeyIps', done => { + it('should succeed if xff header does belong to masterKeyIps', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'], + masterKeyIps: ['10.0.0.1'], }); 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(); - }); + fakeReq.headers['x-forwarded-for'] = '10.0.0.1, 10.0.0.2, ip3'; + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should succeed if xff header with one ip does belong to masterKeyIps', done => { + it('should succeed if xff header with one ip does belong to masterKeyIps', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'], + masterKeyIps: ['10.0.0.1'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; - fakeReq.headers['x-forwarded-for'] = 'ip1'; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeRes.status).not.toHaveBeenCalled(); - done(); - }); + fakeReq.headers['x-forwarded-for'] = '10.0.0.1'; + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); }); - it('should not succeed if xff header does not belong to masterKeyIps', () => { + it('should not succeed if xff header does not belong to masterKeyIps', async () => { 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); + fakeReq.headers['x-forwarded-for'] = '10.0.0.1, 10.0.0.2, ip3'; + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); - it('should not succeed if xff header is empty and masterKeyIps is set', () => { + it('should not succeed if xff header is empty and masterKeyIps is set', async () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'], + masterKeyIps: ['10.0.0.1'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; fakeReq.headers['x-forwarded-for'] = ''; - middlewares.handleParseHeaders(fakeReq, fakeRes); - expect(fakeRes.status).toHaveBeenCalledWith(403); + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(false); }); it('should properly expose the headers', () => { diff --git a/spec/helper.js b/spec/helper.js index a704f9d161..1afa0fc24c 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -110,6 +110,7 @@ const defaultConfiguration = { enableForAnonymousUser: true, enableForAuthenticatedUser: true, }, + masterKeyIps: ['127.0.0.1'], push: { android: { senderId: 'yolo', diff --git a/spec/index.spec.js b/spec/index.spec.js index 837656d1f2..d28d532861 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -495,7 +495,9 @@ describe('server', () => { 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'); + expect(error).toEqual( + 'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".' + ); done(); }); }); diff --git a/src/Config.js b/src/Config.js index c32e53960c..d2cd3b94f8 100644 --- a/src/Config.js +++ b/src/Config.js @@ -435,9 +435,12 @@ export class Config { } static validateMasterKeyIps(masterKeyIps) { - for (const ip of masterKeyIps) { + for (let ip of masterKeyIps) { + if (ip.includes('/')) { + ip = ip.split('/')[0]; + } if (!net.isIP(ip)) { - throw `Invalid ip in masterKeyIps: ${ip}`; + throw `The Parse Server option "masterKeyIps" contains an invalid IP address "${ip}".`; } } } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 465d8aefa3..e25c2e53bc 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -302,9 +302,10 @@ module.exports.ParseServerOptions = { }, masterKeyIps: { env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)', + help: + "(Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key.", action: parsers.arrayParser, - default: [], + default: ['127.0.0.1'], }, maxLimit: { env: 'PARSE_SERVER_MAX_LIMIT', diff --git a/src/Options/docs.js b/src/Options/docs.js index 9a379074b1..6c22e91e2e 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -58,7 +58,7 @@ * @property {String} logLevel Sets the level for logs * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging * @property {String} masterKey Your Parse Master Key - * @property {String[]} masterKeyIps Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) + * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key. * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited * @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) * @property {String} maxUploadSize Max file size for uploads, defaults to 20mb diff --git a/src/Options/index.js b/src/Options/index.js index 87e871e0b0..8b6d4c019e 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -49,8 +49,8 @@ export interface ParseServerOptions { /* URL to your parse server with http:// or https://. :ENV: PARSE_SERVER_URL */ serverURL: string; - /* Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) - :DEFAULT: [] */ + /* (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key. + :DEFAULT: ["127.0.0.1"] */ masterKeyIps: ?(string[]); /* Sets the app name */ appName: ?string; diff --git a/src/middlewares.js b/src/middlewares.js index 37acf46821..42aad1cbbe 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -7,6 +7,7 @@ import defaultLogger from './logger'; import rest from './rest'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter'; +import ipRangeCheck from 'ip-range-check'; export const DEFAULT_ALLOWED_HEADERS = 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; @@ -164,17 +165,11 @@ export function handleParseHeaders(req, res, next) { req.config.ip = clientIp; req.info = info; - if ( - info.masterKey && - req.config.masterKeyIps && - req.config.masterKeyIps.length !== 0 && - req.config.masterKeyIps.indexOf(clientIp) === -1 - ) { - return invalidRequest(req, res); + let isMaster = info.masterKey === req.config.masterKey; + if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) { + isMaster = false; } - var isMaster = info.masterKey === req.config.masterKey; - if (isMaster) { req.auth = new auth.Auth({ config: req.config,