perf: Improved IP validation performance for masterKeyIPs, maintenanceKeyIPs (#8510)
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -30,7 +30,6 @@
|
|||||||
"graphql-relay": "0.10.0",
|
"graphql-relay": "0.10.0",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
"intersect": "1.0.1",
|
"intersect": "1.0.1",
|
||||||
"ip-range-check": "0.2.0",
|
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"jwks-rsa": "2.1.5",
|
"jwks-rsa": "2.1.5",
|
||||||
"ldapjs": "2.3.3",
|
"ldapjs": "2.3.3",
|
||||||
@@ -9250,14 +9249,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||||
},
|
},
|
||||||
"node_modules/ip-range-check": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw==",
|
|
||||||
"dependencies": {
|
|
||||||
"ipaddr.js": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -27583,14 +27574,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||||
},
|
},
|
||||||
"ip-range-check": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw==",
|
|
||||||
"requires": {
|
|
||||||
"ipaddr.js": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ipaddr.js": {
|
"ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
"graphql-relay": "0.10.0",
|
"graphql-relay": "0.10.0",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
"intersect": "1.0.1",
|
"intersect": "1.0.1",
|
||||||
"ip-range-check": "0.2.0",
|
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"jwks-rsa": "2.1.5",
|
"jwks-rsa": "2.1.5",
|
||||||
"ldapjs": "2.3.3",
|
"ldapjs": "2.3.3",
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
const middlewares = require('../lib/middlewares');
|
const middlewares = require('../lib/middlewares');
|
||||||
const AppCache = require('../lib/cache').AppCache;
|
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', () => {
|
describe('middlewares', () => {
|
||||||
let fakeReq, fakeRes;
|
let fakeReq, fakeRes;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeReq = {
|
fakeReq = {
|
||||||
|
ip: '127.0.0.1',
|
||||||
originalUrl: 'http://example.com/parse/',
|
originalUrl: 'http://example.com/parse/',
|
||||||
url: 'http://example.com/',
|
url: 'http://example.com/',
|
||||||
body: {
|
body: {
|
||||||
@@ -16,7 +25,7 @@ describe('middlewares', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
AppCachePut(fakeReq.body._ApplicationId, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -35,7 +44,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
restAPIKey: 'restAPIKey',
|
restAPIKey: 'restAPIKey',
|
||||||
});
|
});
|
||||||
@@ -44,7 +53,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
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',
|
masterKey: 'masterKey',
|
||||||
restAPIKey: 'restAPIKey',
|
restAPIKey: 'restAPIKey',
|
||||||
});
|
});
|
||||||
@@ -54,7 +63,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
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',
|
masterKey: 'masterKey',
|
||||||
restAPIKey: 'restAPIKey',
|
restAPIKey: 'restAPIKey',
|
||||||
});
|
});
|
||||||
@@ -64,7 +73,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should succeed when any one of the configured keys supplied', done => {
|
it('should succeed when any one of the configured keys supplied', done => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
clientKey: 'clientKey',
|
clientKey: 'clientKey',
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
restAPIKey: 'restAPIKey',
|
restAPIKey: 'restAPIKey',
|
||||||
@@ -77,7 +86,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should succeed when client key supplied but empty', done => {
|
it('should succeed when client key supplied but empty', done => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
clientKey: '',
|
clientKey: '',
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
restAPIKey: 'restAPIKey',
|
restAPIKey: 'restAPIKey',
|
||||||
@@ -90,7 +99,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should succeed when no keys are configured and none supplied', done => {
|
it('should succeed when no keys are configured and none supplied', done => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
});
|
});
|
||||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||||
@@ -117,7 +126,7 @@ describe('middlewares', () => {
|
|||||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||||
);
|
);
|
||||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKeyIps: ['0.0.0.0/0'],
|
masterKeyIps: ['0.0.0.0/0'],
|
||||||
});
|
});
|
||||||
fakeReq.ip = '127.0.0.1';
|
fakeReq.ip = '127.0.0.1';
|
||||||
@@ -138,7 +147,7 @@ describe('middlewares', () => {
|
|||||||
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||||
const logger = require('../lib/logger').logger;
|
const logger = require('../lib/logger').logger;
|
||||||
spyOn(logger, 'error').and.callFake(() => {});
|
spyOn(logger, 'error').and.callFake(() => {});
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
masterKeyIps: ['10.0.0.1'],
|
masterKeyIps: ['10.0.0.1'],
|
||||||
});
|
});
|
||||||
@@ -152,7 +161,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
|
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',
|
masterKey: 'masterKey',
|
||||||
masterKeyIps: ['10.0.0.1'],
|
masterKeyIps: ['10.0.0.1'],
|
||||||
});
|
});
|
||||||
@@ -165,7 +174,7 @@ describe('middlewares', () => {
|
|||||||
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||||
const logger = require('../lib/logger').logger;
|
const logger = require('../lib/logger').logger;
|
||||||
spyOn(logger, 'error').and.callFake(() => {});
|
spyOn(logger, 'error').and.callFake(() => {});
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
maintenanceKey: 'masterKey',
|
maintenanceKey: 'masterKey',
|
||||||
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
||||||
});
|
});
|
||||||
@@ -179,7 +188,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
masterKeyIps: ['10.0.0.1'],
|
masterKeyIps: ['10.0.0.1'],
|
||||||
});
|
});
|
||||||
@@ -190,7 +199,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
masterKeyIps: ['0.0.0.0/0'],
|
masterKeyIps: ['0.0.0.0/0'],
|
||||||
});
|
});
|
||||||
@@ -221,7 +230,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
allowHeaders: undefined,
|
allowHeaders: undefined,
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@@ -234,7 +243,7 @@ describe('middlewares', () => {
|
|||||||
allowCrossDomain(fakeReq, res, () => {});
|
allowCrossDomain(fakeReq, res, () => {});
|
||||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||||
|
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
allowHeaders: [],
|
allowHeaders: [],
|
||||||
});
|
});
|
||||||
allowCrossDomain(fakeReq, res, () => {});
|
allowCrossDomain(fakeReq, res, () => {});
|
||||||
@@ -242,7 +251,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
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'],
|
allowHeaders: ['Header-1', 'Header-2'],
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@@ -258,7 +267,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
allowOrigin: undefined,
|
allowOrigin: undefined,
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@@ -273,7 +282,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
|
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/',
|
allowOrigin: 'https://parseplatform.org/',
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@@ -317,7 +326,7 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use user provided on field userFromJWT', done => {
|
it('should use user provided on field userFromJWT', done => {
|
||||||
AppCache.put(fakeReq.body._ApplicationId, {
|
AppCachePut(fakeReq.body._ApplicationId, {
|
||||||
masterKey: 'masterKey',
|
masterKey: 'masterKey',
|
||||||
});
|
});
|
||||||
fakeReq.userFromJWT = 'fake-user';
|
fakeReq.userFromJWT = 'fake-user';
|
||||||
@@ -328,11 +337,87 @@ describe('middlewares', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should give invalid response when upload file without x-parse-application-id in header', () => {
|
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',
|
masterKey: 'masterKey',
|
||||||
});
|
});
|
||||||
fakeReq.body = Buffer.from('fake-file');
|
fakeReq.body = Buffer.from('fake-file');
|
||||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ class ParseServer {
|
|||||||
const allControllers = controllers.getControllers(options);
|
const allControllers = controllers.getControllers(options);
|
||||||
options.state = 'initialized';
|
options.state = 'initialized';
|
||||||
this.config = Config.put(Object.assign({}, options, allControllers));
|
this.config = Config.put(Object.assign({}, options, allControllers));
|
||||||
|
this.config.masterKeyIpsStore = new Map();
|
||||||
|
this.config.maintenanceKeyIpsStore = new Map();
|
||||||
logging.setLogger(allControllers.loggerController);
|
logging.setLogger(allControllers.loggerController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageA
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import { RateLimitOptions } from './Options/Definitions';
|
import { RateLimitOptions } from './Options/Definitions';
|
||||||
import { pathToRegexp } from 'path-to-regexp';
|
import { pathToRegexp } from 'path-to-regexp';
|
||||||
import ipRangeCheck from 'ip-range-check';
|
|
||||||
import RedisStore from 'rate-limit-redis';
|
import RedisStore from 'rate-limit-redis';
|
||||||
import { createClient } from 'redis';
|
import { createClient } from 'redis';
|
||||||
|
import { BlockList, isIPv4 } from 'net';
|
||||||
|
|
||||||
export const DEFAULT_ALLOWED_HEADERS =
|
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';
|
'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';
|
||||||
@@ -23,6 +23,46 @@ const getMountForRequest = function (req) {
|
|||||||
return req.protocol + '://' + req.get('host') + mountPath;
|
return req.protocol + '://' + req.get('host') + mountPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBlockList = (ipRangeList, store) => {
|
||||||
|
if (store.get('blockList')) return store.get('blockList');
|
||||||
|
const blockList = new BlockList();
|
||||||
|
ipRangeList.forEach(fullIp => {
|
||||||
|
if (fullIp === '::/0' || fullIp === '::') {
|
||||||
|
store.set('allowAllIpv6', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fullIp === '0.0.0.0/0' || fullIp === '0.0.0.0') {
|
||||||
|
store.set('allowAllIpv4', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [ip, mask] = fullIp.split('/');
|
||||||
|
if (!mask) {
|
||||||
|
blockList.addAddress(ip, isIPv4(ip) ? 'ipv4' : 'ipv6');
|
||||||
|
} else {
|
||||||
|
blockList.addSubnet(ip, Number(mask), isIPv4(ip) ? 'ipv4' : 'ipv6');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
store.set('blockList', blockList);
|
||||||
|
return blockList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkIp = (ip, ipRangeList, store) => {
|
||||||
|
const incomingIpIsV4 = isIPv4(ip);
|
||||||
|
const blockList = getBlockList(ipRangeList, store);
|
||||||
|
|
||||||
|
if (store.get(ip)) return true;
|
||||||
|
if (store.get('allowAllIpv4') && incomingIpIsV4) return true;
|
||||||
|
if (store.get('allowAllIpv6') && !incomingIpIsV4) return true;
|
||||||
|
const result = blockList.check(ip, incomingIpIsV4 ? 'ipv4' : 'ipv6');
|
||||||
|
|
||||||
|
// If the ip is in the list, we store the result in the store
|
||||||
|
// so we have a optimized path for the next request
|
||||||
|
if (ipRangeList.includes(ip) && result) {
|
||||||
|
store.set(ip, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
// Checks that the request is authorized for this app and checks user
|
// Checks that the request is authorized for this app and checks user
|
||||||
// auth too.
|
// auth too.
|
||||||
// The bodyparser should run before this middleware.
|
// The bodyparser should run before this middleware.
|
||||||
@@ -183,7 +223,7 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
const isMaintenance =
|
const isMaintenance =
|
||||||
req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey;
|
req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey;
|
||||||
if (isMaintenance) {
|
if (isMaintenance) {
|
||||||
if (ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) {
|
if (checkIp(clientIp, req.config.maintenanceKeyIps || [], req.config.maintenanceKeyIpsStore)) {
|
||||||
req.auth = new auth.Auth({
|
req.auth = new auth.Auth({
|
||||||
config: req.config,
|
config: req.config,
|
||||||
installationId: info.installationId,
|
installationId: info.installationId,
|
||||||
@@ -199,7 +239,8 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isMaster = info.masterKey === req.config.masterKey;
|
let isMaster = info.masterKey === req.config.masterKey;
|
||||||
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
|
|
||||||
|
if (isMaster && !checkIp(clientIp, req.config.masterKeyIps || [], req.config.masterKeyIpsStore)) {
|
||||||
const log = req.config?.loggerController || defaultLogger;
|
const log = req.config?.loggerController || defaultLogger;
|
||||||
log.error(
|
log.error(
|
||||||
`Request using master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'masterKeyIps'.`
|
`Request using master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'masterKeyIps'.`
|
||||||
|
|||||||
Reference in New Issue
Block a user