Skip to content

Commit 2b284a4

Browse files
authored
Merge 8ca0279 into 8d3117e
2 parents 8d3117e + 8ca0279 commit 2b284a4

File tree

5 files changed

+143
-42
lines changed

5 files changed

+143
-42
lines changed

package-lock.json

+15-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"graphql-relay": "0.10.0",
4040
"graphql-tag": "2.12.6",
4141
"intersect": "1.0.1",
42-
"ip-range-check": "0.2.0",
4342
"jsonwebtoken": "9.0.0",
4443
"jwks-rsa": "2.1.5",
4544
"ldapjs": "2.3.3",

spec/Middlewares.spec.js

+98-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('middlewares', () => {
2727
expect(fakeReq.headers['content-type']).toEqual(undefined);
2828
const contentType = 'image/jpeg';
2929
fakeReq.body._ContentType = contentType;
30+
fakeReq.ip = '127.0.0.1';
3031
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
3132
expect(fakeReq.headers['content-type']).toEqual(contentType);
3233
expect(fakeReq.body._ContentType).toEqual(undefined);
@@ -151,7 +152,92 @@ describe('middlewares', () => {
151152
);
152153
});
153154

154-
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
155+
it('should match address', () => {
156+
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
157+
const anotherIpv6 = '::ffff:101.10.0.1';
158+
const ipv4 = '192.168.0.101';
159+
const localhostV6 = '::1';
160+
const localhostV62 = '::ffff:127.0.0.1';
161+
const localhostV4 = '127.0.0.1';
162+
163+
const v6 = [ipv6, anotherIpv6];
164+
v6.forEach(ip => {
165+
expect(middlewares.checkIpRanges(ip, ['::/0'])).toBe(true);
166+
expect(middlewares.checkIpRanges(ip, ['::'])).toBe(true);
167+
expect(middlewares.checkIpRanges(ip, ['0.0.0.0'])).toBe(false);
168+
expect(middlewares.checkIpRanges(ip, ['123.123.123.123'])).toBe(false);
169+
});
170+
171+
expect(middlewares.checkIpRanges(ipv6, [anotherIpv6])).toBe(false);
172+
expect(middlewares.checkIpRanges(ipv6, [ipv6])).toBe(true);
173+
expect(middlewares.checkIpRanges(ipv6, ['2001:db8:85a3:0:0:8a2e:0:0/100'])).toBe(true);
174+
175+
expect(middlewares.checkIpRanges(ipv4, ['::'])).toBe(false);
176+
expect(middlewares.checkIpRanges(ipv4, ['::/0'])).toBe(true);
177+
expect(middlewares.checkIpRanges(ipv4, ['0.0.0.0'])).toBe(true);
178+
expect(middlewares.checkIpRanges(ipv4, ['123.123.123.123'])).toBe(false);
179+
expect(middlewares.checkIpRanges(ipv4, [ipv4])).toBe(true);
180+
expect(middlewares.checkIpRanges(ipv4, ['192.168.0.0/24'])).toBe(true);
181+
182+
expect(middlewares.checkIpRanges(localhostV4, ['::1'])).toBe(false);
183+
expect(middlewares.checkIpRanges(localhostV6, ['::1'])).toBe(true);
184+
// ::ffff:127.0.0.1 is a padded ipv4 address but not ::1
185+
expect(middlewares.checkIpRanges(localhostV62, ['::1'])).toBe(false);
186+
// ::ffff:127.0.0.1 is a padded ipv4 address and is a match for 127.0.0.1
187+
expect(middlewares.checkIpRanges(localhostV62, ['127.0.0.1'])).toBe(true);
188+
});
189+
190+
it('can allow all with masterKeyIPs', async () => {
191+
const combinations = [
192+
{
193+
masterKeyIps: ['::/0'],
194+
ips: ['::ffff:192.168.0.101', '192.168.0.101'],
195+
id: 'allowAllIpV6',
196+
},
197+
{
198+
masterKeyIps: ['0.0.0.0'],
199+
ips: ['192.168.0.101'],
200+
id: 'allowAllIpV4',
201+
},
202+
];
203+
for (const combination of combinations) {
204+
AppCache.put(combination.id, {
205+
masterKey: 'masterKey',
206+
masterKeyIps: combination.masterKeyIps,
207+
});
208+
await new Promise(resolve => setTimeout(resolve, 10));
209+
for (const ip of combination.ips) {
210+
fakeReq = {
211+
originalUrl: 'http://example.com/parse/',
212+
url: 'http://example.com/',
213+
body: {
214+
_ApplicationId: combination.id,
215+
},
216+
headers: {},
217+
get: key => {
218+
return fakeReq.headers[key.toLowerCase()];
219+
},
220+
};
221+
fakeReq.ip = ip;
222+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
223+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
224+
expect(fakeReq.auth.isMaster).toBe(true);
225+
}
226+
}
227+
});
228+
229+
it('can allow localhost with masterKeyIPs', async () => {
230+
AppCache.put(fakeReq.body._ApplicationId, {
231+
masterKey: 'masterKey',
232+
masterKeyIps: ['::'],
233+
});
234+
fakeReq.ip = '::ffff:127.0.0.1';
235+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
236+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
237+
expect(fakeReq.auth.isMaster).toBe(true);
238+
});
239+
240+
it('should not succeed if the ip does not belong to masterKeyIps list (ipv4)', async () => {
155241
AppCache.put(fakeReq.body._ApplicationId, {
156242
masterKey: 'masterKey',
157243
masterKeyIps: ['10.0.0.1'],
@@ -162,6 +248,17 @@ describe('middlewares', () => {
162248
expect(fakeReq.auth.isMaster).toBe(false);
163249
});
164250

251+
it('should not succeed if the ip does not belong to masterKeyIps list (ipv6)', async () => {
252+
AppCache.put(fakeReq.body._ApplicationId, {
253+
masterKey: 'masterKey',
254+
masterKeyIps: ['::1'],
255+
});
256+
fakeReq.ip = '::ffff:101.10.0.1';
257+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
258+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
259+
expect(fakeReq.auth.isMaster).toBe(false);
260+
});
261+
165262
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
166263
const logger = require('../lib/logger').logger;
167264
spyOn(logger, 'error').and.callFake(() => {});

spec/index.spec.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,12 @@ describe('server', () => {
450450
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }).catch(done);
451451
});
452452

453-
it('fails if you provides invalid ip in masterKeyIps', done => {
454-
reconfigureServer({ masterKeyIps: ['invalidIp', '1.2.3.4'] }).catch(error => {
455-
expect(error).toEqual(
456-
'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".'
457-
);
458-
done();
459-
});
453+
it('fails if you provides invalid ip in masterKeyIps', async () => {
454+
await expectAsync(
455+
reconfigureServer({ masterKeyIps: ['1.2.3.4/0', 'invalidIp'] })
456+
).toBeRejectedWith(
457+
'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".'
458+
);
460459
});
461460

462461
it('should succeed if you provide valid ip in masterKeyIps', done => {

src/middlewares.js

+24-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageA
1010
import rateLimit from 'express-rate-limit';
1111
import { RateLimitOptions } from './Options/Definitions';
1212
import { pathToRegexp } from 'path-to-regexp';
13-
import ipRangeCheck from 'ip-range-check';
1413
import RedisStore from 'rate-limit-redis';
1514
import { createClient } from 'redis';
15+
import { BlockList, isIPv4 } from 'net';
1616

1717
export const DEFAULT_ALLOWED_HEADERS =
1818
'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,27 @@ const getMountForRequest = function (req) {
2323
return req.protocol + '://' + req.get('host') + mountPath;
2424
};
2525

26+
export const checkIpRanges = (ip, ranges = []) => {
27+
const getType = address => (isIPv4(address) ? 'ipv4' : 'ipv6');
28+
const clientType = getType(ip);
29+
const blocklist = new BlockList();
30+
for (const range of ranges) {
31+
if ((range === '::/0' || range === '::') && clientType === 'ipv6') {
32+
return true;
33+
}
34+
if (range === '0.0.0.0' && clientType === 'ipv4') {
35+
return true;
36+
}
37+
const [addr, prefix] = range.split('/');
38+
if (prefix) {
39+
blocklist.addSubnet(addr, Number(prefix), getType(addr));
40+
} else {
41+
blocklist.addAddress(addr, getType(addr));
42+
}
43+
}
44+
return blocklist.check(ip, clientType);
45+
};
46+
2647
// Checks that the request is authorized for this app and checks user
2748
// auth too.
2849
// The bodyparser should run before this middleware.
@@ -183,7 +204,7 @@ export function handleParseHeaders(req, res, next) {
183204
const isMaintenance =
184205
req.config.maintenanceKey && info.maintenanceKey === req.config.maintenanceKey;
185206
if (isMaintenance) {
186-
if (ipRangeCheck(clientIp, req.config.maintenanceKeyIps || [])) {
207+
if (checkIpRanges(clientIp, req.config.maintenanceKeyIps)) {
187208
req.auth = new auth.Auth({
188209
config: req.config,
189210
installationId: info.installationId,
@@ -199,7 +220,7 @@ export function handleParseHeaders(req, res, next) {
199220
}
200221

201222
let isMaster = info.masterKey === req.config.masterKey;
202-
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
223+
if (isMaster && !checkIpRanges(clientIp, req.config.masterKeyIps)) {
203224
const log = req.config?.loggerController || defaultLogger;
204225
log.error(
205226
`Request using master key rejected as the request IP address '${clientIp}' is not set in Parse Server option 'masterKeyIps'.`

0 commit comments

Comments
 (0)