Skip to content

Commit cbff6b4

Browse files
authored
feat: Add server option readOnlyMasterKeyIps to restrict readOnlyMasterKey by IP (#10115)
1 parent 59ec921 commit cbff6b4

File tree

11 files changed

+103
-1
lines changed

11 files changed

+103
-1
lines changed

DEPRECATIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
1818
| DEPPS12 | Database option `allowPublicExplain` defaults to `false` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | changed | - |
1919
| DEPPS13 | Config option `enableInsecureAuthAdapters` defaults to `false` | [#9667](https://github.com/parse-community/parse-server/pull/9667) | 8.0.0 (2025) | 9.0.0 (2026) | changed | - |
2020
| DEPPS14 | Config option `pages.encodePageParamHeaders` defaults to `true` | [#10063](https://github.com/parse-community/parse-server/issues/10063) | 9.4.0 (2026) | 10.0.0 (2027) | deprecated | - |
21+
| DEPPS15 | Config option `readOnlyMasterKeyIps` defaults to `['127.0.0.1', '::1']` | [#10115](https://github.com/parse-community/parse-server/pull/10115) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
2122

2223
[i_deprecation]: ## "The version and date of the deprecation."
2324
[i_change]: ## "The version and date of the planned change."

spec/Middlewares.spec.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const AppCachePut = (appId, config) =>
77
...config,
88
maintenanceKeyIpsStore: new Map(),
99
masterKeyIpsStore: new Map(),
10+
readOnlyMasterKeyIpsStore: new Map(),
1011
});
1112

1213
describe('middlewares', () => {
@@ -207,6 +208,55 @@ describe('middlewares', () => {
207208
expect(fakeReq.auth.isMaster).toBe(true);
208209
});
209210

211+
it('should not succeed and log if the ip does not belong to readOnlyMasterKeyIps list', async () => {
212+
const logger = require('../lib/logger').logger;
213+
spyOn(logger, 'error').and.callFake(() => {});
214+
AppCachePut(fakeReq.body._ApplicationId, {
215+
masterKeyIps: ['0.0.0.0/0'],
216+
readOnlyMasterKey: 'readOnlyMasterKey',
217+
readOnlyMasterKeyIps: ['10.0.0.1'],
218+
});
219+
fakeReq.ip = '127.0.0.1';
220+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
221+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
222+
223+
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
224+
225+
expect(error).toBeDefined();
226+
expect(error.message).toEqual('unauthorized');
227+
expect(logger.error).toHaveBeenCalledWith(
228+
`Request using read-only master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'readOnlyMasterKeyIps'.`
229+
);
230+
});
231+
232+
it('should succeed if the ip does belong to readOnlyMasterKeyIps list', async () => {
233+
AppCachePut(fakeReq.body._ApplicationId, {
234+
masterKeyIps: ['0.0.0.0/0'],
235+
readOnlyMasterKey: 'readOnlyMasterKey',
236+
readOnlyMasterKeyIps: ['10.0.0.1'],
237+
});
238+
fakeReq.ip = '10.0.0.1';
239+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
240+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
241+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
242+
expect(fakeReq.auth.isMaster).toBe(true);
243+
expect(fakeReq.auth.isReadOnly).toBe(true);
244+
});
245+
246+
it('should allow any ip to use readOnlyMasterKey if readOnlyMasterKeyIps is 0.0.0.0/0', async () => {
247+
AppCachePut(fakeReq.body._ApplicationId, {
248+
masterKeyIps: ['0.0.0.0/0'],
249+
readOnlyMasterKey: 'readOnlyMasterKey',
250+
readOnlyMasterKeyIps: ['0.0.0.0/0'],
251+
});
252+
fakeReq.ip = '10.0.0.1';
253+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
254+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
255+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
256+
expect(fakeReq.auth.isMaster).toBe(true);
257+
expect(fakeReq.auth.isReadOnly).toBe(true);
258+
});
259+
210260
it('can set trust proxy', async () => {
211261
const server = await reconfigureServer({ trustProxy: 1 });
212262
expect(server.app.parent.settings['trust proxy']).toBe(1);

spec/SecurityCheckGroups.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ describe('Security Check Groups', () => {
3535
config.enableInsecureAuthAdapters = false;
3636
config.graphQLPublicIntrospection = false;
3737
config.mountPlayground = false;
38+
config.readOnlyMasterKey = 'someReadOnlyMasterKey';
39+
config.readOnlyMasterKeyIps = ['127.0.0.1', '::1'];
3840
await reconfigureServer(config);
3941

4042
const group = new CheckGroupServerConfig();
@@ -45,6 +47,7 @@ describe('Security Check Groups', () => {
4547
expect(group.checks()[4].checkState()).toBe(CheckState.success);
4648
expect(group.checks()[5].checkState()).toBe(CheckState.success);
4749
expect(group.checks()[6].checkState()).toBe(CheckState.success);
50+
expect(group.checks()[8].checkState()).toBe(CheckState.success);
4851
});
4952

5053
it('checks fail correctly', async () => {
@@ -54,6 +57,8 @@ describe('Security Check Groups', () => {
5457
config.enableInsecureAuthAdapters = true;
5558
config.graphQLPublicIntrospection = true;
5659
config.mountPlayground = true;
60+
config.readOnlyMasterKey = 'someReadOnlyMasterKey';
61+
config.readOnlyMasterKeyIps = ['0.0.0.0/0'];
5762
await reconfigureServer(config);
5863

5964
const group = new CheckGroupServerConfig();
@@ -64,6 +69,7 @@ describe('Security Check Groups', () => {
6469
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
6570
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
6671
expect(group.checks()[6].checkState()).toBe(CheckState.fail);
72+
expect(group.checks()[8].checkState()).toBe(CheckState.fail);
6773
});
6874

6975
it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => {

src/Config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export class Config {
118118
maintenanceKey,
119119
maintenanceKeyIps,
120120
readOnlyMasterKey,
121+
readOnlyMasterKeyIps,
121122
allowHeaders,
122123
idempotencyOptions,
123124
fileUpload,
@@ -158,6 +159,7 @@ export class Config {
158159
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
159160
this.validateIps('masterKeyIps', masterKeyIps);
160161
this.validateIps('maintenanceKeyIps', maintenanceKeyIps);
162+
this.validateIps('readOnlyMasterKeyIps', readOnlyMasterKeyIps);
161163
this.validateDefaultLimit(defaultLimit);
162164
this.validateMaxLimit(maxLimit);
163165
this.validateAllowHeaders(allowHeaders);

src/Deprecator/Deprecations.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@ module.exports = [
2626
changeNewDefault: 'true',
2727
solution: "Set 'pages.encodePageParamHeaders' to 'true' to URI-encode non-ASCII characters in page parameter headers.",
2828
},
29+
{
30+
optionKey: 'readOnlyMasterKeyIps',
31+
changeNewDefault: '["127.0.0.1", "::1"]',
32+
solution: "Set 'readOnlyMasterKeyIps' to the IP addresses that should be allowed to use the read-only master key, or to '[\"127.0.0.1\", \"::1\"]' to restrict access to localhost.",
33+
},
2934
];

src/Options/Definitions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,12 @@ module.exports.ParseServerOptions = {
486486
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
487487
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
488488
},
489+
readOnlyMasterKeyIps: {
490+
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS',
491+
help: "(Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.<br><br>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']`.<br><br><b>Special scenarios:</b><br>- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.<br>- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.<br><br><b>Considerations:</b><br>- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.<br>- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.<br>- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.<br>- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.<br><br>Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`.",
492+
action: parsers.arrayParser,
493+
default: ['0.0.0.0/0', '::0'],
494+
},
489495
requestContextMiddleware: {
490496
env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE',
491497
help: 'Options to customize the request context using inversion of control/dependency injection.',

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export interface ParseServerOptions {
8282
/* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.<br><br>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']`.<br><br><b>Special scenarios:</b><br>- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.<br>- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.<br><br><b>Considerations:</b><br>- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.<br>- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.<br>- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.<br>- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.
8383
:DEFAULT: ["127.0.0.1","::1"] */
8484
maintenanceKeyIps: ?(string[]);
85+
/* (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.<br><br>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']`.<br><br><b>Special scenarios:</b><br>- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.<br>- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.<br><br><b>Considerations:</b><br>- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.<br>- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.<br>- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.<br>- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.<br><br>Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`.
86+
:DEFAULT: ["0.0.0.0/0","::0"] */
87+
readOnlyMasterKeyIps: ?(string[]);
8588
/* Sets the app name */
8689
appName: ?string;
8790
/* Add headers to Access-Control-Allow-Headers */

src/ParseServer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class ParseServer {
137137
this.config = Config.put(Object.assign({}, options, allControllers));
138138
this.config.masterKeyIpsStore = new Map();
139139
this.config.maintenanceKeyIpsStore = new Map();
140+
this.config.readOnlyMasterKeyIpsStore = new Map();
140141
logging.setLogger(allControllers.loggerController);
141142
}
142143

src/Security/CheckGroups/CheckGroupServerConfig.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ class CheckGroupServerConfig extends CheckGroup {
117117
}
118118
},
119119
}),
120+
new Check({
121+
title: 'Read-only master key IP range restricted',
122+
warning:
123+
'The read-only master key can be used from any IP address, which increases the attack surface if the key is compromised.',
124+
solution:
125+
"Change Parse Server configuration to 'readOnlyMasterKeyIps: [\"127.0.0.1\", \"::1\"]' to restrict access to localhost, or set it to a list of specific IP addresses.",
126+
check: () => {
127+
if (!config.readOnlyMasterKey) {
128+
return;
129+
}
130+
const ips = config.readOnlyMasterKeyIps || [];
131+
const wildcards = ['0.0.0.0/0', '0.0.0.0', '::/0', '::', '::0'];
132+
if (ips.some(ip => wildcards.includes(ip))) {
133+
throw 1;
134+
}
135+
},
136+
}),
120137
];
121138
}
122139
}

0 commit comments

Comments
 (0)