Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
50 changes: 50 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const AppCachePut = (appId, config) =>
...config,
maintenanceKeyIpsStore: new Map(),
masterKeyIpsStore: new Map(),
readOnlyMasterKeyIpsStore: new Map(),
});

describe('middlewares', () => {
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions spec/SecurityCheckGroups.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand All @@ -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();
Expand All @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class Config {
maintenanceKey,
maintenanceKeyIps,
readOnlyMasterKey,
readOnlyMasterKeyIps,
allowHeaders,
idempotencyOptions,
fileUpload,
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/Deprecator/Deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
];
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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`.",
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.',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export interface ParseServerOptions {
/* (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.
: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.<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`.
:DEFAULT: ["0.0.0.0/0","::0"] */
readOnlyMasterKeyIps: ?(string[]);
/* Sets the app name */
appName: ?string;
/* Add headers to Access-Control-Allow-Headers */
Expand Down
1 change: 1 addition & 0 deletions src/ParseServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
17 changes: 17 additions & 0 deletions src/Security/CheckGroups/CheckGroupServerConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
},
}),
];
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
Loading