Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Restrict use of masterKey to localhost by default #8281

Merged
merged 16 commits into from
Nov 11, 2022
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
8 changes: 8 additions & 0 deletions package-lock.json

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

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
145 changes: 66 additions & 79 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const AppCache = require('../lib/cache').AppCache;

describe('middlewares', () => {
let fakeReq, fakeRes;

beforeEach(() => {
fakeReq = {
originalUrl: 'http://example.com/parse/',
Expand Down Expand Up @@ -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);
Expand All @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const defaultConfiguration = {
enableForAnonymousUser: true,
enableForAuthenticatedUser: true,
},
masterKeyIps: ['127.0.0.1'],
push: {
android: {
senderId: 'yolo',
Expand Down
4 changes: 3 additions & 1 deletion spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
7 changes: 5 additions & 2 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}".`;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.",
action: parsers.arrayParser,
default: [],
default: ['127.0.0.1'],
},
maxLimit: {
env: 'PARSE_SERVER_MAX_LIMIT',
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.
* @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
Expand Down
4 changes: 2 additions & 2 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.
:DEFAULT: ["127.0.0.1"] */
masterKeyIps: ?(string[]);
/* Sets the app name */
appName: ?string;
Expand Down
13 changes: 4 additions & 9 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down