Skip to content

Commit

Permalink
perf: Improved IP validation performance for masterKeyIPs, `mainten…
Browse files Browse the repository at this point in the history
…anceKeyIPs` (#8510)
  • Loading branch information
Moumouls committed Nov 19, 2023
1 parent 7597319 commit b87daba
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 41 deletions.
17 changes: 0 additions & 17 deletions package-lock.json

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

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"graphql-relay": "0.10.0",
"graphql-tag": "2.12.6",
"intersect": "1.0.1",
"ip-range-check": "0.2.0",
"jsonwebtoken": "9.0.0",
"jwks-rsa": "2.1.5",
"ldapjs": "2.3.3",
Expand Down
125 changes: 105 additions & 20 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
const middlewares = require('../lib/middlewares');
const AppCache = require('../lib/cache').AppCache;
const { BlockList } = require('net');

const AppCachePut = (appId, config) =>
AppCache.put(appId, {
...config,
maintenanceKeyIpsStore: new Map(),
masterKeyIpsStore: new Map(),
});

describe('middlewares', () => {
let fakeReq, fakeRes;
beforeEach(() => {
fakeReq = {
ip: '127.0.0.1',
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: {
Expand All @@ -16,7 +25,7 @@ describe('middlewares', () => {
},
};
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
AppCache.put(fakeReq.body._ApplicationId, {});
AppCachePut(fakeReq.body._ApplicationId, {});
});

afterEach(() => {
Expand All @@ -35,7 +44,7 @@ describe('middlewares', () => {
});

it('should give invalid response when keys are configured but no key supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
Expand All @@ -44,7 +53,7 @@ describe('middlewares', () => {
});

it('should give invalid response when keys are configured but supplied key is incorrect', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
Expand All @@ -54,7 +63,7 @@ describe('middlewares', () => {
});

it('should give invalid response when keys are configured but different key is supplied', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
Expand All @@ -64,7 +73,7 @@ describe('middlewares', () => {
});

it('should succeed when any one of the configured keys supplied', done => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
clientKey: 'clientKey',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
Expand All @@ -77,7 +86,7 @@ describe('middlewares', () => {
});

it('should succeed when client key supplied but empty', done => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
clientKey: '',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
Expand All @@ -90,7 +99,7 @@ describe('middlewares', () => {
});

it('should succeed when no keys are configured and none supplied', done => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
});
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
Expand All @@ -117,7 +126,7 @@ describe('middlewares', () => {
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
);
it(`it should pull ${bodyKey} into req.info`, done => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.ip = '127.0.0.1';
Expand All @@ -138,7 +147,7 @@ describe('middlewares', () => {
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['10.0.0.1'],
});
Expand All @@ -152,7 +161,7 @@ describe('middlewares', () => {
});

it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['10.0.0.1'],
});
Expand All @@ -165,7 +174,7 @@ describe('middlewares', () => {
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
maintenanceKey: 'masterKey',
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
});
Expand All @@ -179,7 +188,7 @@ describe('middlewares', () => {
});

it('should succeed if the ip does belong to masterKeyIps list', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['10.0.0.1'],
});
Expand All @@ -190,7 +199,7 @@ describe('middlewares', () => {
});

it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
});
Expand Down Expand Up @@ -221,7 +230,7 @@ describe('middlewares', () => {
});

it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
allowHeaders: undefined,
});
const headers = {};
Expand All @@ -234,15 +243,15 @@ describe('middlewares', () => {
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);

AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
allowHeaders: [],
});
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
});

it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
allowHeaders: ['Header-1', 'Header-2'],
});
const headers = {};
Expand All @@ -258,7 +267,7 @@ describe('middlewares', () => {
});

it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
allowOrigin: undefined,
});
const headers = {};
Expand All @@ -273,7 +282,7 @@ describe('middlewares', () => {
});

it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
allowOrigin: 'https://parseplatform.org/',
});
const headers = {};
Expand Down Expand Up @@ -317,7 +326,7 @@ describe('middlewares', () => {
});

it('should use user provided on field userFromJWT', done => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
});
fakeReq.userFromJWT = 'fake-user';
Expand All @@ -328,11 +337,87 @@ describe('middlewares', () => {
});

it('should give invalid response when upload file without x-parse-application-id in header', () => {
AppCache.put(fakeReq.body._ApplicationId, {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
});
fakeReq.body = Buffer.from('fake-file');
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should match address', () => {
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
const anotherIpv6 = '::ffff:101.10.0.1';
const ipv4 = '192.168.0.101';
const localhostV6 = '::1';
const localhostV62 = '::ffff:127.0.0.1';
const localhostV4 = '127.0.0.1';

const v6 = [ipv6, anotherIpv6];
v6.forEach(ip => {
expect(middlewares.checkIp(ip, ['::/0'], new Map())).toBe(true);
expect(middlewares.checkIp(ip, ['::'], new Map())).toBe(true);
expect(middlewares.checkIp(ip, ['0.0.0.0'], new Map())).toBe(false);
expect(middlewares.checkIp(ip, ['0.0.0.0/0'], new Map())).toBe(false);
expect(middlewares.checkIp(ip, ['123.123.123.123'], new Map())).toBe(false);
});

expect(middlewares.checkIp(ipv6, [anotherIpv6], new Map())).toBe(false);
expect(middlewares.checkIp(ipv6, [ipv6], new Map())).toBe(true);
expect(middlewares.checkIp(ipv6, ['2001:db8:85a3:0:0:8a2e:0:0/100'], new Map())).toBe(true);

expect(middlewares.checkIp(ipv4, ['::'], new Map())).toBe(false);
expect(middlewares.checkIp(ipv4, ['::/0'], new Map())).toBe(false);
expect(middlewares.checkIp(ipv4, ['0.0.0.0'], new Map())).toBe(true);
expect(middlewares.checkIp(ipv4, ['0.0.0.0/0'], new Map())).toBe(true);
expect(middlewares.checkIp(ipv4, ['123.123.123.123'], new Map())).toBe(false);
expect(middlewares.checkIp(ipv4, [ipv4], new Map())).toBe(true);
expect(middlewares.checkIp(ipv4, ['192.168.0.0/24'], new Map())).toBe(true);

expect(middlewares.checkIp(localhostV4, ['::1'], new Map())).toBe(false);
expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true);
// ::ffff:127.0.0.1 is a padded ipv4 address but not ::1
expect(middlewares.checkIp(localhostV62, ['::1'], new Map())).toBe(false);
// ::ffff:127.0.0.1 is a padded ipv4 address and is a match for 127.0.0.1
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
});

it('should match address with cache', () => {
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
const cache1 = new Map();
const spyBlockListCheck = spyOn(BlockList.prototype, 'check').and.callThrough();
expect(middlewares.checkIp(ipv6, ['::'], cache1)).toBe(true);
expect(cache1.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined);
expect(cache1.get('allowAllIpv6')).toBe(true);
expect(spyBlockListCheck).toHaveBeenCalledTimes(0);

const cache2 = new Map();
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
expect(cache2.get('::1')).toBe(true);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
spyBlockListCheck.calls.reset();

const cache3 = new Map();
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
expect(cache3.get('127.0.0.1')).toBe(true);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
spyBlockListCheck.calls.reset();

const cache4 = new Map();
const ranges = ['127.0.0.1', '192.168.0.0/24'];
// should not cache negative match
expect(middlewares.checkIp('123.123.123.123', ranges, cache4)).toBe(false);
expect(cache4.get('123.123.123.123')).toBe(undefined);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
spyBlockListCheck.calls.reset();

// should not cache cidr
expect(middlewares.checkIp('192.168.0.101', ranges, cache4)).toBe(true);
expect(cache4.get('192.168.0.101')).toBe(undefined);
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class ParseServer {
const allControllers = controllers.getControllers(options);
options.state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map();
this.config.maintenanceKeyIpsStore = new Map();
logging.setLogger(allControllers.loggerController);
}

Expand Down
Loading

0 comments on commit b87daba

Please sign in to comment.