Skip to content

Commit

Permalink
Merge 8ca0279 into 8d3117e
Browse files Browse the repository at this point in the history
  • Loading branch information
dblythy authored Oct 7, 2023
2 parents 8d3117e + 8ca0279 commit 2b284a4
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 42 deletions.
45 changes: 15 additions & 30 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
99 changes: 98 additions & 1 deletion spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('middlewares', () => {
expect(fakeReq.headers['content-type']).toEqual(undefined);
const contentType = 'image/jpeg';
fakeReq.body._ContentType = contentType;
fakeReq.ip = '127.0.0.1';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.headers['content-type']).toEqual(contentType);
expect(fakeReq.body._ContentType).toEqual(undefined);
Expand Down Expand Up @@ -151,7 +152,92 @@ describe('middlewares', () => {
);
});

it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
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.checkIpRanges(ip, ['::/0'])).toBe(true);
expect(middlewares.checkIpRanges(ip, ['::'])).toBe(true);
expect(middlewares.checkIpRanges(ip, ['0.0.0.0'])).toBe(false);
expect(middlewares.checkIpRanges(ip, ['123.123.123.123'])).toBe(false);
});

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

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

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

it('can allow all with masterKeyIPs', async () => {
const combinations = [
{
masterKeyIps: ['::/0'],
ips: ['::ffff:192.168.0.101', '192.168.0.101'],
id: 'allowAllIpV6',
},
{
masterKeyIps: ['0.0.0.0'],
ips: ['192.168.0.101'],
id: 'allowAllIpV4',
},
];
for (const combination of combinations) {
AppCache.put(combination.id, {
masterKey: 'masterKey',
masterKeyIps: combination.masterKeyIps,
});
await new Promise(resolve => setTimeout(resolve, 10));
for (const ip of combination.ips) {
fakeReq = {
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: {
_ApplicationId: combination.id,
},
headers: {},
get: key => {
return fakeReq.headers[key.toLowerCase()];
},
};
fakeReq.ip = ip;
fakeReq.headers['x-parse-master-key'] = 'masterKey';
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
expect(fakeReq.auth.isMaster).toBe(true);
}
}
});

it('can allow localhost with masterKeyIPs', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['::'],
});
fakeReq.ip = '::ffff:127.0.0.1';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
expect(fakeReq.auth.isMaster).toBe(true);
});

it('should not succeed if the ip does not belong to masterKeyIps list (ipv4)', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['10.0.0.1'],
Expand All @@ -162,6 +248,17 @@ describe('middlewares', () => {
expect(fakeReq.auth.isMaster).toBe(false);
});

it('should not succeed if the ip does not belong to masterKeyIps list (ipv6)', async () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['::1'],
});
fakeReq.ip = '::ffff:101.10.0.1';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
expect(fakeReq.auth.isMaster).toBe(false);
});

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(() => {});
Expand Down
13 changes: 6 additions & 7 deletions spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,12 @@ describe('server', () => {
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }).catch(done);
});

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

it('should succeed if you provide valid ip in masterKeyIps', done => {
Expand Down
27 changes: 24 additions & 3 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ipRangeCheck from 'ip-range-check';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
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';
Expand All @@ -23,6 +23,27 @@ const getMountForRequest = function (req) {
return req.protocol + '://' + req.get('host') + mountPath;
};

export const checkIpRanges = (ip, ranges = []) => {
const getType = address => (isIPv4(address) ? 'ipv4' : 'ipv6');
const clientType = getType(ip);
const blocklist = new BlockList();
for (const range of ranges) {
if ((range === '::/0' || range === '::') && clientType === 'ipv6') {
return true;
}
if (range === '0.0.0.0' && clientType === 'ipv4') {
return true;
}
const [addr, prefix] = range.split('/');
if (prefix) {
blocklist.addSubnet(addr, Number(prefix), getType(addr));
} else {
blocklist.addAddress(addr, getType(addr));
}
}
return blocklist.check(ip, clientType);
};

// Checks that the request is authorized for this app and checks user
// auth too.
// The bodyparser should run before this middleware.
Expand Down Expand Up @@ -183,7 +204,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 (checkIpRanges(clientIp, req.config.maintenanceKeyIps)) {
req.auth = new auth.Auth({
config: req.config,
installationId: info.installationId,
Expand All @@ -199,7 +220,7 @@ export function handleParseHeaders(req, res, next) {
}

let isMaster = info.masterKey === req.config.masterKey;
if (isMaster && !ipRangeCheck(clientIp, req.config.masterKeyIps || [])) {
if (isMaster && !checkIpRanges(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'.`
Expand Down

0 comments on commit 2b284a4

Please sign in to comment.