diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index 707901612f..d3e4825d20 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -18,6 +18,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| 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 | - |
| 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 | - |
| 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 | - |
+| 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 | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js
index 57dca22b0e..8e1e04f8e5 100644
--- a/spec/Middlewares.spec.js
+++ b/spec/Middlewares.spec.js
@@ -7,6 +7,7 @@ const AppCachePut = (appId, config) =>
...config,
maintenanceKeyIpsStore: new Map(),
masterKeyIpsStore: new Map(),
+ readOnlyMasterKeyIpsStore: new Map(),
});
describe('middlewares', () => {
@@ -207,6 +208,55 @@ describe('middlewares', () => {
expect(fakeReq.auth.isMaster).toBe(true);
});
+ it('should not succeed and log if the ip does not belong to readOnlyMasterKeyIps list', async () => {
+ const logger = require('../lib/logger').logger;
+ spyOn(logger, 'error').and.callFake(() => {});
+ AppCachePut(fakeReq.body._ApplicationId, {
+ masterKeyIps: ['0.0.0.0/0'],
+ readOnlyMasterKey: 'readOnlyMasterKey',
+ readOnlyMasterKeyIps: ['10.0.0.1'],
+ });
+ fakeReq.ip = '127.0.0.1';
+ fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
+ fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
+
+ const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
+
+ expect(error).toBeDefined();
+ expect(error.message).toEqual('unauthorized');
+ expect(logger.error).toHaveBeenCalledWith(
+ `Request using read-only master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'readOnlyMasterKeyIps'.`
+ );
+ });
+
+ it('should succeed if the ip does belong to readOnlyMasterKeyIps list', async () => {
+ AppCachePut(fakeReq.body._ApplicationId, {
+ masterKeyIps: ['0.0.0.0/0'],
+ readOnlyMasterKey: 'readOnlyMasterKey',
+ readOnlyMasterKeyIps: ['10.0.0.1'],
+ });
+ fakeReq.ip = '10.0.0.1';
+ fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
+ fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
+ await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
+ expect(fakeReq.auth.isMaster).toBe(true);
+ expect(fakeReq.auth.isReadOnly).toBe(true);
+ });
+
+ it('should allow any ip to use readOnlyMasterKey if readOnlyMasterKeyIps is 0.0.0.0/0', async () => {
+ AppCachePut(fakeReq.body._ApplicationId, {
+ masterKeyIps: ['0.0.0.0/0'],
+ readOnlyMasterKey: 'readOnlyMasterKey',
+ readOnlyMasterKeyIps: ['0.0.0.0/0'],
+ });
+ fakeReq.ip = '10.0.0.1';
+ fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
+ fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
+ await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
+ expect(fakeReq.auth.isMaster).toBe(true);
+ expect(fakeReq.auth.isReadOnly).toBe(true);
+ });
+
it('can set trust proxy', async () => {
const server = await reconfigureServer({ trustProxy: 1 });
expect(server.app.parent.settings['trust proxy']).toBe(1);
diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js
index 4f4d5fa6d9..c9a87ac8ca 100644
--- a/spec/SecurityCheckGroups.spec.js
+++ b/spec/SecurityCheckGroups.spec.js
@@ -35,6 +35,8 @@ describe('Security Check Groups', () => {
config.enableInsecureAuthAdapters = false;
config.graphQLPublicIntrospection = false;
config.mountPlayground = false;
+ config.readOnlyMasterKey = 'someReadOnlyMasterKey';
+ config.readOnlyMasterKeyIps = ['127.0.0.1', '::1'];
await reconfigureServer(config);
const group = new CheckGroupServerConfig();
@@ -45,6 +47,7 @@ describe('Security Check Groups', () => {
expect(group.checks()[4].checkState()).toBe(CheckState.success);
expect(group.checks()[5].checkState()).toBe(CheckState.success);
expect(group.checks()[6].checkState()).toBe(CheckState.success);
+ expect(group.checks()[8].checkState()).toBe(CheckState.success);
});
it('checks fail correctly', async () => {
@@ -54,6 +57,8 @@ describe('Security Check Groups', () => {
config.enableInsecureAuthAdapters = true;
config.graphQLPublicIntrospection = true;
config.mountPlayground = true;
+ config.readOnlyMasterKey = 'someReadOnlyMasterKey';
+ config.readOnlyMasterKeyIps = ['0.0.0.0/0'];
await reconfigureServer(config);
const group = new CheckGroupServerConfig();
@@ -64,6 +69,7 @@ describe('Security Check Groups', () => {
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
expect(group.checks()[6].checkState()).toBe(CheckState.fail);
+ expect(group.checks()[8].checkState()).toBe(CheckState.fail);
});
it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => {
diff --git a/src/Config.js b/src/Config.js
index d5ea54691c..a3a56ea0f7 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -118,6 +118,7 @@ export class Config {
maintenanceKey,
maintenanceKeyIps,
readOnlyMasterKey,
+ readOnlyMasterKeyIps,
allowHeaders,
idempotencyOptions,
fileUpload,
@@ -158,6 +159,7 @@ export class Config {
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
this.validateIps('masterKeyIps', masterKeyIps);
this.validateIps('maintenanceKeyIps', maintenanceKeyIps);
+ this.validateIps('readOnlyMasterKeyIps', readOnlyMasterKeyIps);
this.validateDefaultLimit(defaultLimit);
this.validateMaxLimit(maxLimit);
this.validateAllowHeaders(allowHeaders);
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 9b57f1be44..9093018f48 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -26,4 +26,9 @@ module.exports = [
changeNewDefault: 'true',
solution: "Set 'pages.encodePageParamHeaders' to 'true' to URI-encode non-ASCII characters in page parameter headers.",
},
+ {
+ optionKey: 'readOnlyMasterKeyIps',
+ changeNewDefault: '["127.0.0.1", "::1"]',
+ 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.",
+ },
];
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 7b28fc5c26..f265f57c99 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -486,6 +486,12 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
},
+ readOnlyMasterKeyIps: {
+ env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS',
+ help: "(Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.
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 scenarios:
- 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.
- 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.
Considerations:
- 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.
- 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.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
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`.",
+ action: parsers.arrayParser,
+ default: ['0.0.0.0/0', '::0'],
+ },
requestContextMiddleware: {
env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE',
help: 'Options to customize the request context using inversion of control/dependency injection.',
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 8d0ad9445c..8278838340 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -91,6 +91,7 @@
* @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications
* @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.
ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.
* @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes
+ * @property {String[]} readOnlyMasterKeyIps (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.
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 scenarios:
- 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.
- 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.
Considerations:
- 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.
- 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.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
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`.
* @property {Function} requestContextMiddleware Options to customize the request context using inversion of control/dependency injection.
* @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
* @property {String} restAPIKey Key for REST calls
diff --git a/src/Options/index.js b/src/Options/index.js
index 83277a76d6..96c94f9df1 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -82,6 +82,9 @@ export interface ParseServerOptions {
/* (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.
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 scenarios:
- 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.
- 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.
Considerations:
- 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.
- 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.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
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.
:DEFAULT: ["127.0.0.1","::1"] */
maintenanceKeyIps: ?(string[]);
+ /* (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.
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 scenarios:
- 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.
- 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.
Considerations:
- 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.
- 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.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
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`.
+ :DEFAULT: ["0.0.0.0/0","::0"] */
+ readOnlyMasterKeyIps: ?(string[]);
/* Sets the app name */
appName: ?string;
/* Add headers to Access-Control-Allow-Headers */
diff --git a/src/ParseServer.ts b/src/ParseServer.ts
index 2e37c4728c..e1672aff5c 100644
--- a/src/ParseServer.ts
+++ b/src/ParseServer.ts
@@ -137,6 +137,7 @@ class ParseServer {
this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map();
this.config.maintenanceKeyIpsStore = new Map();
+ this.config.readOnlyMasterKeyIpsStore = new Map();
logging.setLogger(allControllers.loggerController);
}
diff --git a/src/Security/CheckGroups/CheckGroupServerConfig.js b/src/Security/CheckGroups/CheckGroupServerConfig.js
index 0d1bc8c92b..76e441d572 100644
--- a/src/Security/CheckGroups/CheckGroupServerConfig.js
+++ b/src/Security/CheckGroups/CheckGroupServerConfig.js
@@ -117,6 +117,23 @@ class CheckGroupServerConfig extends CheckGroup {
}
},
}),
+ new Check({
+ title: 'Read-only master key IP range restricted',
+ warning:
+ 'The read-only master key can be used from any IP address, which increases the attack surface if the key is compromised.',
+ solution:
+ "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.",
+ check: () => {
+ if (!config.readOnlyMasterKey) {
+ return;
+ }
+ const ips = config.readOnlyMasterKeyIps || [];
+ const wildcards = ['0.0.0.0/0', '0.0.0.0', '::/0', '::', '::0'];
+ if (ips.some(ip => wildcards.includes(ip))) {
+ throw 1;
+ }
+ },
+ }),
];
}
}
diff --git a/src/middlewares.js b/src/middlewares.js
index 95b40aa7dd..06ed72e7ae 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -28,7 +28,7 @@ const getBlockList = (ipRangeList, store) => {
if (store.get('blockList')) { return store.get('blockList'); }
const blockList = new BlockList();
ipRangeList.forEach(fullIp => {
- if (fullIp === '::/0' || fullIp === '::') {
+ if (fullIp === '::/0' || fullIp === '::' || fullIp === '::0') {
store.set('allowAllIpv6', true);
return;
}
@@ -270,6 +270,16 @@ export async function handleParseHeaders(req, res, next) {
req.config.readOnlyMasterKey &&
isReadOnlyMaster
) {
+ if (!checkIp(clientIp, req.config.readOnlyMasterKeyIps || [], req.config.readOnlyMasterKeyIpsStore)) {
+ const log = req.config?.loggerController || defaultLogger;
+ log.error(
+ `Request using read-only master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'readOnlyMasterKeyIps'.`
+ );
+ const error = new Error();
+ error.status = 403;
+ error.message = 'unauthorized';
+ throw error;
+ }
req.auth = new auth.Auth({
config: req.config,
installationId: info.installationId,