From 2e795e65f0fd967c1f184a37f58310888b0f51e1 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 14 Apr 2023 15:56:06 +0200 Subject: [PATCH 01/10] feat: change ip package --- package-lock.json | 66 ++++++++++++++++++++-------------------------- package.json | 2 +- src/middlewares.js | 8 +++--- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18f5a16b4e..0eac031e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip-range-check": "0.2.0", + "ip": "1.1.8", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", @@ -9156,17 +9156,9 @@ } }, "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "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" - } + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -11489,12 +11481,14 @@ } }, "node_modules/mock-files-adapter": { - "resolved": "spec/dependencies/mock-files-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-files-adapter", + "dev": true }, "node_modules/mock-mail-adapter": { - "resolved": "spec/dependencies/mock-mail-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-mail-adapter", + "dev": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -18427,6 +18421,11 @@ "npm": ">= 3.0.0" } }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -20454,14 +20453,6 @@ "dependencies": { "zen-observable": "0.8.15" } - }, - "spec/dependencies/mock-files-adapter": { - "version": "1.0.0", - "dev": true - }, - "spec/dependencies/mock-mail-adapter": { - "version": "1.0.0", - "dev": true } }, "dependencies": { @@ -27331,17 +27322,9 @@ } }, "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "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" - } + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, "ipaddr.js": { "version": "1.9.1", @@ -29166,10 +29149,12 @@ } }, "mock-files-adapter": { - "version": "file:spec/dependencies/mock-files-adapter" + "version": "1.0.0", + "dev": true }, "mock-mail-adapter": { - "version": "file:spec/dependencies/mock-mail-adapter" + "version": "1.0.0", + "dev": true }, "modify-values": { "version": "1.0.1", @@ -34364,6 +34349,13 @@ "requires": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" + }, + "dependencies": { + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + } } }, "sort-keys": { diff --git a/package.json b/package.json index fc2dbe6250..533d5ee9de 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip-range-check": "0.2.0", + "ip": "1.1.8", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", diff --git a/src/middlewares.js b/src/middlewares.js index 0dca33135e..b7cf4a1d47 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -10,7 +10,7 @@ import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageA import rateLimit from 'express-rate-limit'; import { RateLimitOptions } from './Options/Definitions'; import pathToRegexp from 'path-to-regexp'; -import ipRangeCheck from 'ip-range-check'; +import { cidrSubnet } from 'ip'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; @@ -23,6 +23,8 @@ const getMountForRequest = function (req) { return req.protocol + '://' + req.get('host') + mountPath; }; +const checkRanges = (allowedIps, ip) => allowedIps.some(range => cidrSubnet(range).contains(ip)); + // Checks that the request is authorized for this app and checks user // auth too. // The bodyparser should run before this middleware. @@ -183,7 +185,7 @@ export function handleParseHeaders(req, res, next) { const isMaintenance = req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey; if (isMaintenance) { - if (ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) { + if (checkRanges(clientIp, req.config.maintenanceKeyIps || [])) { req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, @@ -199,7 +201,7 @@ export function handleParseHeaders(req, res, next) { } let isMaster = info.masterKey === req.config.masterKey; - if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) { + if (isMaster && !checkRanges(clientIp, req.config.masterKeyIps || [])) { const log = req.config?.loggerController || defaultLogger; log.error( `Request using master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'masterKeyIps'.` From a13768df5eb23f54d331fc5e01eff2538a47c755 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 14 Apr 2023 19:39:43 +0200 Subject: [PATCH 02/10] fix: ip address handling --- package-lock.json | 26 +++++------------ package.json | 2 +- spec/Middlewares.spec.js | 63 ++++++++++++++++++++++++++++++++++++++++ src/ParseServer.js | 2 ++ src/middlewares.js | 35 +++++++++++++++++++--- 5 files changed, 104 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0eac031e63..4398878bb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip": "1.1.8", + "ip": "2.0.0", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", @@ -9156,9 +9156,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -18421,11 +18421,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -27322,9 +27317,9 @@ } }, "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, "ipaddr.js": { "version": "1.9.1", @@ -34349,13 +34344,6 @@ "requires": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" - }, - "dependencies": { - "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - } } }, "sort-keys": { diff --git a/package.json b/package.json index 533d5ee9de..ffb144cee4 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip": "1.1.8", + "ip": "2.0.0", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 12bfc59bf7..4aa5eebc1c 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -306,4 +306,67 @@ describe('middlewares', () => { 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, ['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::/81'], 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, ['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); + expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true); + 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); + // but match the ipv4 address + 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 cache = new Map(); + // should not cache allow all + expect(middlewares.checkIp(ipv6, ['::'], cache)).toBe(true); + expect(cache.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined); + expect(middlewares.checkIp('::1', ['::1'], cache)).toBe(true); + expect(cache.get('::1')).toBe(true); + // should cache positive match + expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache)).toBe(true); + expect(cache.get('127.0.0.1')).toBe(true); + // should use the cache + const ipRangeList = ['127.0.0.1']; + const spy = spyOn(ipRangeList, 'some').and.callThrough(); + expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache)).toBe(true); + expect(spy).not.toHaveBeenCalled(); + + // should not cache negative match + expect(middlewares.checkIp('123.123.123.123', ['127.0.0.1'], cache)).toBe(false); + expect(cache.get('123.123.123.123')).toBe(undefined); + + // should not cache cidr + expect(middlewares.checkIp('192.168.0.101', ['192.168.0.0/24'], cache)).toBe(true); + expect(cache.get('192.168.0.101')).toBe(undefined); + }); }); diff --git a/src/ParseServer.js b/src/ParseServer.js index 04379ecfd3..1d4496492e 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -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); } diff --git a/src/middlewares.js b/src/middlewares.js index b7cf4a1d47..f3dd114334 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -10,9 +10,9 @@ import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageA import rateLimit from 'express-rate-limit'; import { RateLimitOptions } from './Options/Definitions'; import pathToRegexp from 'path-to-regexp'; -import { cidrSubnet } from 'ip'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; +import { cidrSubnet, isEqual, isV4Format } from 'ip'; 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'; @@ -23,7 +23,33 @@ const getMountForRequest = function (req) { return req.protocol + '://' + req.get('host') + mountPath; }; -const checkRanges = (allowedIps, ip) => allowedIps.some(range => cidrSubnet(range).contains(ip)); +export const checkIp = (ip, ipRangeList, store) => { + if (store && store.get(ip)) { + return true; + } + return ipRangeList.some(range => { + const ipIsV4 = isV4Format(ip); + if (ipIsV4 && range === '0.0.0.0') { + return true; + } + if (!ipIsV4 && range === '::') { + return true; + } + const isASimpleIp = range.indexOf('/') === -1; + if (isASimpleIp) { + const result = isEqual(ip, range); + // We can optimize the next call by storing the positive result + // it's safe since the store will only grow to the number of unique ips mentioned in the config + if (result && store) store.set(ip, result); + return result; + } + // Do not allow cross version subnet matching + if (ipIsV4 !== isV4Format(range.split('/')[0])) { + return false; + } + return cidrSubnet(range).contains(ip); + }); +}; // Checks that the request is authorized for this app and checks user // auth too. @@ -185,7 +211,7 @@ export function handleParseHeaders(req, res, next) { const isMaintenance = req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey; if (isMaintenance) { - if (checkRanges(clientIp, req.config.maintenanceKeyIps || [])) { + if (checkIp(clientIp, req.config.maintenanceKeyIps || [], req.config.maintenanceKeyIpsStore)) { req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, @@ -201,7 +227,8 @@ export function handleParseHeaders(req, res, next) { } let isMaster = info.masterKey === req.config.masterKey; - if (isMaster && !checkRanges(clientIp, req.config.masterKeyIps || [])) { + + if (isMaster && !checkIp(clientIp, req.config.masterKeyIps || [], req.config.masterKeyIpsStore)) { const log = req.config?.loggerController || defaultLogger; log.error( `Request using master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'masterKeyIps'.` From cd54e42ee6b42790f1a5608f1dfee24edc16b11a Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 14 Apr 2023 19:48:16 +0200 Subject: [PATCH 03/10] fix: lint --- package-lock.json | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4398878bb3..69aa543766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11481,14 +11481,12 @@ } }, "node_modules/mock-files-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-files-adapter", - "dev": true + "resolved": "spec/dependencies/mock-files-adapter", + "link": true }, "node_modules/mock-mail-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-mail-adapter", - "dev": true + "resolved": "spec/dependencies/mock-mail-adapter", + "link": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -20448,6 +20446,14 @@ "dependencies": { "zen-observable": "0.8.15" } + }, + "spec/dependencies/mock-files-adapter": { + "version": "1.0.0", + "dev": true + }, + "spec/dependencies/mock-mail-adapter": { + "version": "1.0.0", + "dev": true } }, "dependencies": { @@ -29144,12 +29150,10 @@ } }, "mock-files-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-files-adapter" }, "mock-mail-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-mail-adapter" }, "modify-values": { "version": "1.0.1", From 57791ddabfbb63fc9a4d43f43c37c6d59e3baf93 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 16 Apr 2023 12:26:11 +0200 Subject: [PATCH 04/10] feat: native module with cache feature --- spec/Middlewares.spec.js | 106 +++++++++++++++++++++++---------------- src/middlewares.js | 58 ++++++++++++--------- 2 files changed, 98 insertions(+), 66 deletions(-) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 4aa5eebc1c..cbaafd15be 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -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: { @@ -16,7 +25,7 @@ describe('middlewares', () => { }, }; fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']); - AppCache.put(fakeReq.body._ApplicationId, {}); + AppCachePut(fakeReq.body._ApplicationId, {}); }); afterEach(() => { @@ -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', }); @@ -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', }); @@ -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', }); @@ -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', @@ -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', @@ -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, () => { @@ -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'; @@ -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'], }); @@ -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'], }); @@ -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'], }); @@ -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'], }); @@ -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'], }); @@ -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 = {}; @@ -234,7 +243,7 @@ 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, () => {}); @@ -242,7 +251,7 @@ describe('middlewares', () => { }); 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 = {}; @@ -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 = {}; @@ -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 = {}; @@ -288,7 +297,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'; @@ -299,7 +308,7 @@ 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'); @@ -325,7 +334,7 @@ describe('middlewares', () => { 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::/81'], 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); @@ -336,37 +345,48 @@ describe('middlewares', () => { expect(middlewares.checkIp(localhostV4, ['::1'], new Map())).toBe(false); expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true); - expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true); - 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); - // but match the ipv4 address + // ::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 cache = new Map(); - // should not cache allow all - expect(middlewares.checkIp(ipv6, ['::'], cache)).toBe(true); - expect(cache.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined); - expect(middlewares.checkIp('::1', ['::1'], cache)).toBe(true); - expect(cache.get('::1')).toBe(true); - // should cache positive match - expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache)).toBe(true); - expect(cache.get('127.0.0.1')).toBe(true); - // should use the cache - const ipRangeList = ['127.0.0.1']; - const spy = spyOn(ipRangeList, 'some').and.callThrough(); - expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache)).toBe(true); - expect(spy).not.toHaveBeenCalled(); - + 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', ['127.0.0.1'], cache)).toBe(false); - expect(cache.get('123.123.123.123')).toBe(undefined); + 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', ['192.168.0.0/24'], cache)).toBe(true); - expect(cache.get('192.168.0.101')).toBe(undefined); + expect(middlewares.checkIp('192.168.0.101', ranges, cache4)).toBe(true); + expect(cache4.get('192.168.0.101')).toBe(undefined); + expect(spyBlockListCheck).toHaveBeenCalledTimes(1); }); }); diff --git a/src/middlewares.js b/src/middlewares.js index f3dd114334..b9d1e00850 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -12,7 +12,7 @@ import { RateLimitOptions } from './Options/Definitions'; import pathToRegexp from 'path-to-regexp'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; -import { cidrSubnet, isEqual, isV4Format } from 'ip'; +import { BlockList, isIPv4 } from 'net'; 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'; @@ -23,32 +23,44 @@ const getMountForRequest = function (req) { return req.protocol + '://' + req.get('host') + mountPath; }; -export const checkIp = (ip, ipRangeList, store) => { - if (store && store.get(ip)) { - return true; - } - return ipRangeList.some(range => { - const ipIsV4 = isV4Format(ip); - if (ipIsV4 && range === '0.0.0.0') { - return true; - } - if (!ipIsV4 && range === '::') { - return true; +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; } - const isASimpleIp = range.indexOf('/') === -1; - if (isASimpleIp) { - const result = isEqual(ip, range); - // We can optimize the next call by storing the positive result - // it's safe since the store will only grow to the number of unique ips mentioned in the config - if (result && store) store.set(ip, result); - return result; + if (fullIp === '0.0.0.0') { + store.set('allowAllIpv4', true); + return; } - // Do not allow cross version subnet matching - if (ipIsV4 !== isV4Format(range.split('/')[0])) { - return false; + const [ip, mask] = fullIp.split('/'); + if (!mask) { + blockList.addAddress(ip, isIPv4(ip) ? 'ipv4' : 'ipv6'); + } else { + blockList.addSubnet(ip, Number(mask), isIPv4(ip) ? 'ipv4' : 'ipv6'); } - return cidrSubnet(range).contains(ip); }); + 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 From fec2e5dd0ff1321b8b5fc05fc63be2d6456154a7 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 16 Apr 2023 12:29:44 +0200 Subject: [PATCH 05/10] fix: remove package --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69aa543766..d54db146ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip": "2.0.0", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", diff --git a/package.json b/package.json index ffb144cee4..bd93a45577 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", "intersect": "1.0.1", - "ip": "2.0.0", "jsonwebtoken": "9.0.0", "jwks-rsa": "2.1.5", "ldapjs": "2.3.3", From d9b0d590592b15dba93e4f81996076adf4117fbc Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 6 Jul 2023 09:51:18 +0200 Subject: [PATCH 06/10] fix: remove useless import --- package-lock.json | 26 +++++++++++--------------- src/middlewares.js | 1 - 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ee633fa07..7b79c229d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", - "otpauth": "^9.1.2", + "otpauth": "9.1.2", "parse": "4.1.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", @@ -11684,12 +11684,14 @@ } }, "node_modules/mock-files-adapter": { - "resolved": "spec/dependencies/mock-files-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-files-adapter", + "dev": true }, "node_modules/mock-mail-adapter": { - "resolved": "spec/dependencies/mock-mail-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-mail-adapter", + "dev": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -20698,14 +20700,6 @@ "dependencies": { "zen-observable": "0.8.15" } - }, - "spec/dependencies/mock-files-adapter": { - "version": "1.0.0", - "dev": true - }, - "spec/dependencies/mock-mail-adapter": { - "version": "1.0.0", - "dev": true } }, "dependencies": { @@ -29572,10 +29566,12 @@ } }, "mock-files-adapter": { - "version": "file:spec/dependencies/mock-files-adapter" + "version": "1.0.0", + "dev": true }, "mock-mail-adapter": { - "version": "file:spec/dependencies/mock-mail-adapter" + "version": "1.0.0", + "dev": true }, "modify-values": { "version": "1.0.1", diff --git a/src/middlewares.js b/src/middlewares.js index 95de6b2cec..38219e7bf8 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -10,7 +10,6 @@ import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageA import rateLimit from 'express-rate-limit'; import { RateLimitOptions } from './Options/Definitions'; import { pathToRegexp } from 'path-to-regexp'; -import ipRangeCheck from 'ip-range-check'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; import { BlockList, isIPv4 } from 'net'; From 330e1d5faab7c3ba0438c3d088a48c525d50fd4e Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 6 Jul 2023 09:56:27 +0200 Subject: [PATCH 07/10] fix: lock --- package-lock.json | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b79c229d1..e6f126b066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11684,14 +11684,12 @@ } }, "node_modules/mock-files-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-files-adapter", - "dev": true + "resolved": "spec/dependencies/mock-files-adapter", + "link": true }, "node_modules/mock-mail-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-mail-adapter", - "dev": true + "resolved": "spec/dependencies/mock-mail-adapter", + "link": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -20700,6 +20698,14 @@ "dependencies": { "zen-observable": "0.8.15" } + }, + "spec/dependencies/mock-files-adapter": { + "version": "1.0.0", + "dev": true + }, + "spec/dependencies/mock-mail-adapter": { + "version": "1.0.0", + "dev": true } }, "dependencies": { @@ -29566,12 +29572,10 @@ } }, "mock-files-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-files-adapter" }, "mock-mail-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-mail-adapter" }, "modify-values": { "version": "1.0.1", From 12862904af3353e1ccc9463316d3b5dc2d176077 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:16:23 +0100 Subject: [PATCH 08/10] Update spec/Middlewares.spec.js Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/Middlewares.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 40a1fe1bb9..588017e4d1 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -358,6 +358,7 @@ describe('middlewares', () => { 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); }); From a634da7c5f5990e81546da880a4652f9495d22d1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:16:31 +0100 Subject: [PATCH 09/10] Update spec/Middlewares.spec.js Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/Middlewares.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 588017e4d1..7ec50bd434 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -369,6 +369,7 @@ describe('middlewares', () => { 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); From 3d378a5879e8cd3c6df69c7be6f0a0325ebb9160 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:25:21 +0100 Subject: [PATCH 10/10] Update src/middlewares.js Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/middlewares.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares.js b/src/middlewares.js index 38219e7bf8..9319130188 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -31,7 +31,7 @@ const getBlockList = (ipRangeList, store) => { store.set('allowAllIpv6', true); return; } - if (fullIp === '0.0.0.0') { + if (fullIp === '0.0.0.0/0' || fullIp === '0.0.0.0') { store.set('allowAllIpv4', true); return; }