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,