Skip to content

Commit 6c16021

Browse files
authored
feat: Restrict use of masterKey to localhost by default (#8281)
BREAKING CHANGE: This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281)
1 parent 7336afc commit 6c16021

10 files changed

+98
-100
lines changed

Diff for: package-lock.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
],
2020
"license": "BSD-3-Clause",
2121
"dependencies": {
22-
"@graphql-yoga/node": "2.6.0",
23-
"@graphql-tools/utils": "8.12.0",
2422
"@graphql-tools/merge": "8.3.6",
2523
"@graphql-tools/schema": "9.0.4",
24+
"@graphql-tools/utils": "8.12.0",
25+
"@graphql-yoga/node": "2.6.0",
2626
"@parse/fs-files-adapter": "1.2.2",
2727
"@parse/push-adapter": "4.1.2",
2828
"bcryptjs": "2.4.3",
@@ -34,9 +34,10 @@
3434
"follow-redirects": "1.15.2",
3535
"graphql": "16.6.0",
3636
"graphql-list-fields": "2.0.2",
37-
"graphql-tag": "2.12.6",
3837
"graphql-relay": "0.10.0",
38+
"graphql-tag": "2.12.6",
3939
"intersect": "1.0.1",
40+
"ip-range-check": "0.2.0",
4041
"jsonwebtoken": "8.5.1",
4142
"jwks-rsa": "2.1.5",
4243
"ldapjs": "2.3.3",
@@ -59,7 +60,6 @@
5960
"ws": "8.9.0"
6061
},
6162
"devDependencies": {
62-
"graphql-tag": "2.12.6",
6363
"@actions/core": "1.9.1",
6464
"@apollo/client": "3.6.1",
6565
"@babel/cli": "7.10.0",
@@ -86,6 +86,7 @@
8686
"eslint-plugin-flowtype": "5.1.3",
8787
"flow-bin": "0.119.1",
8888
"form-data": "3.0.0",
89+
"graphql-tag": "2.12.6",
8990
"husky": "4.3.8",
9091
"jasmine": "3.5.0",
9192
"jasmine-spec-reporter": "7.0.0",

Diff for: spec/Middlewares.spec.js

+66-79
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const AppCache = require('../lib/cache').AppCache;
33

44
describe('middlewares', () => {
55
let fakeReq, fakeRes;
6-
76
beforeEach(() => {
87
fakeReq = {
98
originalUrl: 'http://example.com/parse/',
@@ -117,10 +116,12 @@ describe('middlewares', () => {
117116
const otherKeys = BodyKeys.filter(
118117
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
119118
);
120-
121119
it(`it should pull ${bodyKey} into req.info`, done => {
120+
AppCache.put(fakeReq.body._ApplicationId, {
121+
masterKeyIps: ['0.0.0.0/0'],
122+
});
123+
fakeReq.ip = '127.0.0.1';
122124
fakeReq.body[bodyKey] = keyValue;
123-
124125
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
125126
expect(fakeReq.body[bodyKey]).toEqual(undefined);
126127
expect(fakeReq.info[infoKey]).toEqual(keyValue);
@@ -134,161 +135,147 @@ describe('middlewares', () => {
134135
});
135136
});
136137

137-
it('should not succeed if the ip does not belong to masterKeyIps list', () => {
138+
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
138139
AppCache.put(fakeReq.body._ApplicationId, {
139140
masterKey: 'masterKey',
140-
masterKeyIps: ['ip1', 'ip2'],
141+
masterKeyIps: ['10.0.0.1'],
141142
});
142-
fakeReq.ip = 'ip3';
143+
fakeReq.ip = '127.0.0.1';
143144
fakeReq.headers['x-parse-master-key'] = 'masterKey';
144-
middlewares.handleParseHeaders(fakeReq, fakeRes);
145-
expect(fakeRes.status).toHaveBeenCalledWith(403);
145+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
146+
expect(fakeReq.auth.isMaster).toBe(false);
146147
});
147148

148-
it('should succeed if the ip does belong to masterKeyIps list', done => {
149+
it('should succeed if the ip does belong to masterKeyIps list', async () => {
149150
AppCache.put(fakeReq.body._ApplicationId, {
150151
masterKey: 'masterKey',
151-
masterKeyIps: ['ip1', 'ip2'],
152+
masterKeyIps: ['10.0.0.1'],
152153
});
153-
fakeReq.ip = 'ip1';
154+
fakeReq.ip = '10.0.0.1';
154155
fakeReq.headers['x-parse-master-key'] = 'masterKey';
155-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
156-
expect(fakeRes.status).not.toHaveBeenCalled();
157-
done();
158-
});
156+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
157+
expect(fakeReq.auth.isMaster).toBe(true);
159158
});
160159

161-
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
160+
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', async () => {
162161
AppCache.put(fakeReq.body._ApplicationId, {
163162
masterKey: 'masterKey',
164-
masterKeyIps: ['ip1', 'ip2'],
163+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
165164
});
166-
fakeReq.connection = { remoteAddress: 'ip3' };
165+
fakeReq.connection = { remoteAddress: '127.0.0.1' };
167166
fakeReq.headers['x-parse-master-key'] = 'masterKey';
168-
middlewares.handleParseHeaders(fakeReq, fakeRes);
169-
expect(fakeRes.status).toHaveBeenCalledWith(403);
167+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
168+
expect(fakeReq.auth.isMaster).toBe(false);
170169
});
171170

172-
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
171+
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', async () => {
173172
AppCache.put(fakeReq.body._ApplicationId, {
174173
masterKey: 'masterKey',
175-
masterKeyIps: ['ip1', 'ip2'],
174+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
176175
});
177-
fakeReq.connection = { remoteAddress: 'ip1' };
176+
fakeReq.connection = { remoteAddress: '10.0.0.1' };
178177
fakeReq.headers['x-parse-master-key'] = 'masterKey';
179-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
180-
expect(fakeRes.status).not.toHaveBeenCalled();
181-
done();
182-
});
178+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
179+
expect(fakeReq.auth.isMaster).toBe(true);
183180
});
184181

185-
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
182+
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', async () => {
186183
AppCache.put(fakeReq.body._ApplicationId, {
187184
masterKey: 'masterKey',
188-
masterKeyIps: ['ip1', 'ip2'],
185+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
189186
});
190-
fakeReq.socket = { remoteAddress: 'ip3' };
187+
fakeReq.socket = { remoteAddress: '127.0.0.1' };
191188
fakeReq.headers['x-parse-master-key'] = 'masterKey';
192-
middlewares.handleParseHeaders(fakeReq, fakeRes);
193-
expect(fakeRes.status).toHaveBeenCalledWith(403);
189+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
190+
expect(fakeReq.auth.isMaster).toBe(false);
194191
});
195192

196-
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
193+
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', async () => {
197194
AppCache.put(fakeReq.body._ApplicationId, {
198195
masterKey: 'masterKey',
199-
masterKeyIps: ['ip1', 'ip2'],
196+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
200197
});
201-
fakeReq.socket = { remoteAddress: 'ip1' };
198+
fakeReq.socket = { remoteAddress: '10.0.0.1' };
202199
fakeReq.headers['x-parse-master-key'] = 'masterKey';
203-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
204-
expect(fakeRes.status).not.toHaveBeenCalled();
205-
done();
206-
});
200+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
201+
expect(fakeReq.auth.isMaster).toBe(true);
207202
});
208203

209-
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
204+
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', async () => {
210205
AppCache.put(fakeReq.body._ApplicationId, {
211206
masterKey: 'masterKey',
212-
masterKeyIps: ['ip1', 'ip2'],
207+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
213208
});
214209
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
215210
fakeReq.headers['x-parse-master-key'] = 'masterKey';
216-
middlewares.handleParseHeaders(fakeReq, fakeRes);
217-
expect(fakeRes.status).toHaveBeenCalledWith(403);
211+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
212+
expect(fakeReq.auth.isMaster).toBe(false);
218213
});
219214

220-
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
215+
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', async () => {
221216
AppCache.put(fakeReq.body._ApplicationId, {
222217
masterKey: 'masterKey',
223-
masterKeyIps: ['ip1', 'ip2'],
218+
masterKeyIps: ['10.0.0.1', '10.0.0.2'],
224219
});
225-
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
220+
fakeReq.connection = { socket: { remoteAddress: '10.0.0.1' } };
226221
fakeReq.headers['x-parse-master-key'] = 'masterKey';
227-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
228-
expect(fakeRes.status).not.toHaveBeenCalled();
229-
done();
230-
});
222+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
223+
expect(fakeReq.auth.isMaster).toBe(true);
231224
});
232225

233-
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
226+
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
234227
AppCache.put(fakeReq.body._ApplicationId, {
235228
masterKey: 'masterKey',
236-
masterKeyIps: [],
229+
masterKeyIps: ['0.0.0.0/0'],
237230
});
238-
fakeReq.ip = 'ip1';
231+
fakeReq.ip = '10.0.0.1';
239232
fakeReq.headers['x-parse-master-key'] = 'masterKey';
240-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
241-
expect(fakeRes.status).not.toHaveBeenCalled();
242-
done();
243-
});
233+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
234+
expect(fakeReq.auth.isMaster).toBe(true);
244235
});
245236

246-
it('should succeed if xff header does belong to masterKeyIps', done => {
237+
it('should succeed if xff header does belong to masterKeyIps', async () => {
247238
AppCache.put(fakeReq.body._ApplicationId, {
248239
masterKey: 'masterKey',
249-
masterKeyIps: ['ip1'],
240+
masterKeyIps: ['10.0.0.1'],
250241
});
251242
fakeReq.headers['x-parse-master-key'] = 'masterKey';
252-
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
253-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
254-
expect(fakeRes.status).not.toHaveBeenCalled();
255-
done();
256-
});
243+
fakeReq.headers['x-forwarded-for'] = '10.0.0.1, 10.0.0.2, ip3';
244+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
245+
expect(fakeReq.auth.isMaster).toBe(true);
257246
});
258247

259-
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
248+
it('should succeed if xff header with one ip does belong to masterKeyIps', async () => {
260249
AppCache.put(fakeReq.body._ApplicationId, {
261250
masterKey: 'masterKey',
262-
masterKeyIps: ['ip1'],
251+
masterKeyIps: ['10.0.0.1'],
263252
});
264253
fakeReq.headers['x-parse-master-key'] = 'masterKey';
265-
fakeReq.headers['x-forwarded-for'] = 'ip1';
266-
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
267-
expect(fakeRes.status).not.toHaveBeenCalled();
268-
done();
269-
});
254+
fakeReq.headers['x-forwarded-for'] = '10.0.0.1';
255+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
256+
expect(fakeReq.auth.isMaster).toBe(true);
270257
});
271258

272-
it('should not succeed if xff header does not belong to masterKeyIps', () => {
259+
it('should not succeed if xff header does not belong to masterKeyIps', async () => {
273260
AppCache.put(fakeReq.body._ApplicationId, {
274261
masterKey: 'masterKey',
275262
masterKeyIps: ['ip4'],
276263
});
277264
fakeReq.headers['x-parse-master-key'] = 'masterKey';
278-
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
279-
middlewares.handleParseHeaders(fakeReq, fakeRes);
280-
expect(fakeRes.status).toHaveBeenCalledWith(403);
265+
fakeReq.headers['x-forwarded-for'] = '10.0.0.1, 10.0.0.2, ip3';
266+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
267+
expect(fakeReq.auth.isMaster).toBe(false);
281268
});
282269

283-
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
270+
it('should not succeed if xff header is empty and masterKeyIps is set', async () => {
284271
AppCache.put(fakeReq.body._ApplicationId, {
285272
masterKey: 'masterKey',
286-
masterKeyIps: ['ip1'],
273+
masterKeyIps: ['10.0.0.1'],
287274
});
288275
fakeReq.headers['x-parse-master-key'] = 'masterKey';
289276
fakeReq.headers['x-forwarded-for'] = '';
290-
middlewares.handleParseHeaders(fakeReq, fakeRes);
291-
expect(fakeRes.status).toHaveBeenCalledWith(403);
277+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
278+
expect(fakeReq.auth.isMaster).toBe(false);
292279
});
293280

294281
it('should properly expose the headers', () => {

Diff for: spec/helper.js

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const defaultConfiguration = {
110110
enableForAnonymousUser: true,
111111
enableForAuthenticatedUser: true,
112112
},
113+
masterKeyIps: ['127.0.0.1'],
113114
push: {
114115
android: {
115116
senderId: 'yolo',

Diff for: spec/index.spec.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,9 @@ describe('server', () => {
495495

496496
it('fails if you provides invalid ip in masterKeyIps', done => {
497497
reconfigureServer({ masterKeyIps: ['invalidIp', '1.2.3.4'] }).catch(error => {
498-
expect(error).toEqual('Invalid ip in masterKeyIps: invalidIp');
498+
expect(error).toEqual(
499+
'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".'
500+
);
499501
done();
500502
});
501503
});

Diff for: src/Config.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,12 @@ export class Config {
435435
}
436436

437437
static validateMasterKeyIps(masterKeyIps) {
438-
for (const ip of masterKeyIps) {
438+
for (let ip of masterKeyIps) {
439+
if (ip.includes('/')) {
440+
ip = ip.split('/')[0];
441+
}
439442
if (!net.isIP(ip)) {
440-
throw `Invalid ip in masterKeyIps: ${ip}`;
443+
throw `The Parse Server option "masterKeyIps" contains an invalid IP address "${ip}".`;
441444
}
442445
}
443446
}

Diff for: src/Options/Definitions.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,10 @@ module.exports.ParseServerOptions = {
302302
},
303303
masterKeyIps: {
304304
env: 'PARSE_SERVER_MASTER_KEY_IPS',
305-
help: 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)',
305+
help:
306+
"(Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>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.<br><br>Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key.",
306307
action: parsers.arrayParser,
307-
default: [],
308+
default: ['127.0.0.1'],
308309
},
309310
maxLimit: {
310311
env: 'PARSE_SERVER_MAX_LIMIT',

Diff for: src/Options/docs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
* @property {String} logLevel Sets the level for logs
5959
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
6060
* @property {String} masterKey Your Parse Master Key
61-
* @property {String[]} masterKeyIps Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)
61+
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>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.<br><br>Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key.
6262
* @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited
6363
* @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)
6464
* @property {String} maxUploadSize Max file size for uploads, defaults to 20mb

Diff for: src/Options/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export interface ParseServerOptions {
4949
/* URL to your parse server with http:// or https://.
5050
:ENV: PARSE_SERVER_URL */
5151
serverURL: string;
52-
/* Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)
53-
:DEFAULT: [] */
52+
/* (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>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.<br><br>Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key.
53+
:DEFAULT: ["127.0.0.1"] */
5454
masterKeyIps: ?(string[]);
5555
/* Sets the app name */
5656
appName: ?string;

Diff for: src/middlewares.js

+4-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import defaultLogger from './logger';
77
import rest from './rest';
88
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
99
import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter';
10+
import ipRangeCheck from 'ip-range-check';
1011

1112
export const DEFAULT_ALLOWED_HEADERS =
1213
'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) {
164165
req.config.ip = clientIp;
165166
req.info = info;
166167

167-
if (
168-
info.masterKey &&
169-
req.config.masterKeyIps &&
170-
req.config.masterKeyIps.length !== 0 &&
171-
req.config.masterKeyIps.indexOf(clientIp) === -1
172-
) {
173-
return invalidRequest(req, res);
168+
let isMaster = info.masterKey === req.config.masterKey;
169+
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
170+
isMaster = false;
174171
}
175172

176-
var isMaster = info.masterKey === req.config.masterKey;
177-
178173
if (isMaster) {
179174
req.auth = new auth.Auth({
180175
config: req.config,

0 commit comments

Comments
 (0)