Skip to content

Commit 7e54265

Browse files
pungmeflovilmart
authored andcommitted
Security: limit Masterkey remote access (#4017)
* update choose_password to have the confirmation * add comment mark * First version, no test * throw error right away instead of just use masterKey false * fix the logic * move it up before the masterKey check * adding some test * typo * remove the choose_password * newline * add cli options * remove trailing space * handle in case the server is behind proxy * add getting the first ip in the ip list of xff * sanity check the ip in config if it is a valid ip address * split ip extraction to another function * trailing spaces
1 parent 811d8b0 commit 7e54265

File tree

7 files changed

+223
-2
lines changed

7 files changed

+223
-2
lines changed

spec/Middlewares.spec.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,161 @@ describe('middlewares', () => {
133133
});
134134
});
135135
});
136+
137+
it('should not succeed if the ip does not belong to masterKeyIps list', () => {
138+
AppCache.put(fakeReq.body._ApplicationId, {
139+
masterKey: 'masterKey',
140+
masterKeyIps: ['ip1','ip2']
141+
});
142+
fakeReq.ip = 'ip3';
143+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
144+
middlewares.handleParseHeaders(fakeReq, fakeRes);
145+
expect(fakeRes.status).toHaveBeenCalledWith(403);
146+
});
147+
148+
it('should succeed if the ip does belong to masterKeyIps list', (done) => {
149+
AppCache.put(fakeReq.body._ApplicationId, {
150+
masterKey: 'masterKey',
151+
masterKeyIps: ['ip1','ip2']
152+
});
153+
fakeReq.ip = 'ip1';
154+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
155+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
156+
expect(fakeRes.status).not.toHaveBeenCalled();
157+
done();
158+
});
159+
});
160+
161+
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
162+
AppCache.put(fakeReq.body._ApplicationId, {
163+
masterKey: 'masterKey',
164+
masterKeyIps: ['ip1','ip2']
165+
});
166+
fakeReq.connection = {remoteAddress : 'ip3'};
167+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
168+
middlewares.handleParseHeaders(fakeReq, fakeRes);
169+
expect(fakeRes.status).toHaveBeenCalledWith(403);
170+
});
171+
172+
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', (done) => {
173+
AppCache.put(fakeReq.body._ApplicationId, {
174+
masterKey: 'masterKey',
175+
masterKeyIps: ['ip1','ip2']
176+
});
177+
fakeReq.connection = {remoteAddress : 'ip1'};
178+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
179+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
180+
expect(fakeRes.status).not.toHaveBeenCalled();
181+
done();
182+
});
183+
});
184+
185+
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
186+
AppCache.put(fakeReq.body._ApplicationId, {
187+
masterKey: 'masterKey',
188+
masterKeyIps: ['ip1','ip2']
189+
});
190+
fakeReq.socket = {remoteAddress : 'ip3'};
191+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
192+
middlewares.handleParseHeaders(fakeReq, fakeRes);
193+
expect(fakeRes.status).toHaveBeenCalledWith(403);
194+
});
195+
196+
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', (done) => {
197+
AppCache.put(fakeReq.body._ApplicationId, {
198+
masterKey: 'masterKey',
199+
masterKeyIps: ['ip1','ip2']
200+
});
201+
fakeReq.socket = {remoteAddress : 'ip1'};
202+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
203+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
204+
expect(fakeRes.status).not.toHaveBeenCalled();
205+
done();
206+
});
207+
});
208+
209+
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
210+
AppCache.put(fakeReq.body._ApplicationId, {
211+
masterKey: 'masterKey',
212+
masterKeyIps: ['ip1','ip2']
213+
});
214+
fakeReq.connection = { socket : {remoteAddress : 'ip3'}};
215+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
216+
middlewares.handleParseHeaders(fakeReq, fakeRes);
217+
expect(fakeRes.status).toHaveBeenCalledWith(403);
218+
});
219+
220+
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', (done) => {
221+
AppCache.put(fakeReq.body._ApplicationId, {
222+
masterKey: 'masterKey',
223+
masterKeyIps: ['ip1','ip2']
224+
});
225+
fakeReq.connection = { socket : {remoteAddress : 'ip1'}};
226+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
227+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
228+
expect(fakeRes.status).not.toHaveBeenCalled();
229+
done();
230+
});
231+
});
232+
233+
it('should allow any ip to use masterKey if masterKeyIps is empty', (done) => {
234+
AppCache.put(fakeReq.body._ApplicationId, {
235+
masterKey: 'masterKey',
236+
masterKeyIps: []
237+
});
238+
fakeReq.ip = 'ip1';
239+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
240+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
241+
expect(fakeRes.status).not.toHaveBeenCalled();
242+
done();
243+
});
244+
});
245+
246+
it('should succeed if xff header does belong to masterKeyIps', (done) => {
247+
AppCache.put(fakeReq.body._ApplicationId, {
248+
masterKey: 'masterKey',
249+
masterKeyIps: ['ip1']
250+
});
251+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
252+
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
253+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
254+
expect(fakeRes.status).not.toHaveBeenCalled();
255+
done();
256+
});
257+
});
258+
259+
it('should succeed if xff header with one ip does belong to masterKeyIps', (done) => {
260+
AppCache.put(fakeReq.body._ApplicationId, {
261+
masterKey: 'masterKey',
262+
masterKeyIps: ['ip1']
263+
});
264+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
265+
fakeReq.headers['x-forwarded-for'] = 'ip1';
266+
middlewares.handleParseHeaders(fakeReq, fakeRes,() => {
267+
expect(fakeRes.status).not.toHaveBeenCalled();
268+
done();
269+
});
270+
});
271+
272+
it('should not succeed if xff header does not belong to masterKeyIps', () => {
273+
AppCache.put(fakeReq.body._ApplicationId, {
274+
masterKey: 'masterKey',
275+
masterKeyIps: ['ip4']
276+
});
277+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
278+
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
279+
middlewares.handleParseHeaders(fakeReq, fakeRes);
280+
expect(fakeRes.status).toHaveBeenCalledWith(403);
281+
});
282+
283+
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
284+
AppCache.put(fakeReq.body._ApplicationId, {
285+
masterKey: 'masterKey',
286+
masterKeyIps: ['ip1']
287+
});
288+
fakeReq.headers['x-parse-master-key'] = 'masterKey';
289+
fakeReq.headers['x-forwarded-for'] = '';
290+
middlewares.handleParseHeaders(fakeReq, fakeRes);
291+
expect(fakeRes.status).toHaveBeenCalledWith(403);
292+
});
136293
});

spec/index.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,18 @@ describe('server', () => {
419419
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' })
420420
.catch(done);
421421
});
422+
423+
it('fails if you provides invalid ip in masterKeyIps', done => {
424+
reconfigureServer({ masterKeyIps: ['invalidIp','1.2.3.4'] })
425+
.catch(error => {
426+
expect(error).toEqual('Invalid ip in masterKeyIps: invalidIp');
427+
done();
428+
})
429+
});
430+
431+
it('should suceed if you provide valid ip in masterKeyIps', done => {
432+
reconfigureServer({ masterKeyIps: ['1.2.3.4','2001:0db8:0000:0042:0000:8a2e:0370:7334'] })
433+
.then(done)
434+
});
435+
422436
});

src/Config.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import AppCache from './cache';
66
import SchemaCache from './Controllers/SchemaCache';
77
import DatabaseController from './Controllers/DatabaseController';
8+
import net from 'net';
89

910
function removeTrailingSlash(str) {
1011
if (!str) {
@@ -26,6 +27,7 @@ export class Config {
2627
this.applicationId = applicationId;
2728
this.jsonLogs = cacheInfo.jsonLogs;
2829
this.masterKey = cacheInfo.masterKey;
30+
this.masterKeyIps = cacheInfo.masterKeyIps;
2931
this.clientKey = cacheInfo.clientKey;
3032
this.javascriptKey = cacheInfo.javascriptKey;
3133
this.dotNetKey = cacheInfo.dotNetKey;
@@ -86,7 +88,8 @@ export class Config {
8688
sessionLength,
8789
emailVerifyTokenValidityDuration,
8890
accountLockout,
89-
passwordPolicy
91+
passwordPolicy,
92+
masterKeyIps
9093
}) {
9194
const emailAdapter = userController.adapter;
9295
if (verifyUserEmails) {
@@ -108,6 +111,8 @@ export class Config {
108111
}
109112

110113
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
114+
115+
this.validateMasterKeyIps(masterKeyIps);
111116
}
112117

113118
static validateAccountLockoutPolicy(accountLockout) {
@@ -184,6 +189,14 @@ export class Config {
184189
}
185190
}
186191

192+
static validateMasterKeyIps(masterKeyIps) {
193+
for (const ip of masterKeyIps) {
194+
if(!net.isIP(ip)){
195+
throw `Invalid ip in masterKeyIps: ${ip}`;
196+
}
197+
}
198+
}
199+
187200
get mount() {
188201
var mount = this._mount;
189202
if (this.publicServerURL) {

src/ParseServer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class ParseServer {
9292
constructor({
9393
appId = requiredParameter('You must provide an appId!'),
9494
masterKey = requiredParameter('You must provide a masterKey!'),
95+
masterKeyIps = [],
9596
appName,
9697
analyticsAdapter,
9798
filesAdapter,
@@ -167,6 +168,11 @@ class ParseServer {
167168
userSensitiveFields
168169
)));
169170

171+
masterKeyIps = Array.from(new Set(masterKeyIps.concat(
172+
defaults.masterKeyIps,
173+
masterKeyIps
174+
)));
175+
170176
const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, { jsonLogs, logsFolder, verbose, logLevel, silent });
171177
const loggerController = new LoggerController(loggerControllerAdapter, appId);
172178
logging.setLogger(loggerController);
@@ -228,6 +234,7 @@ class ParseServer {
228234
AppCache.put(appId, {
229235
appId,
230236
masterKey: masterKey,
237+
masterKeyIps:masterKeyIps,
231238
serverURL: serverURL,
232239
collectionPrefix: collectionPrefix,
233240
clientKey: clientKey,

src/cli/definitions/parse-server.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export default {
1919
help: "Your Parse Master Key",
2020
required: true
2121
},
22+
"masterKeyIps": {
23+
env: "PARSE_SERVER_MASTER_KEY_IPS",
24+
help: "Restrict masterKey to be used by only these ips. defaults to [] (allow all ips)",
25+
default: []
26+
},
2227
"port": {
2328
env: "PORT",
2429
help: "The port to run the ParseServer. defaults to 1337.",

src/defaults.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ export default {
3535
cacheTTL: 5000,
3636
cacheMaxSize: 10000,
3737
userSensitiveFields: ['email'],
38-
objectIdSize: 10
38+
objectIdSize: 10,
39+
masterKeyIps: []
3940
}

src/middlewares.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ export function handleParseHeaders(req, res, next) {
111111
req.config.headers = req.headers || {};
112112
req.info = info;
113113

114+
const ip = getClientIp(req);
115+
if (info.masterKey && req.config.masterKeyIps && req.config.masterKeyIps.length !== 0 && req.config.masterKeyIps.indexOf(ip) === -1) {
116+
return invalidRequest(req, res);
117+
}
118+
114119
var isMaster = (info.masterKey === req.config.masterKey);
115120

116121
if (isMaster) {
@@ -171,6 +176,25 @@ export function handleParseHeaders(req, res, next) {
171176
});
172177
}
173178

179+
function getClientIp(req){
180+
if (req.headers['x-forwarded-for']) {
181+
// try to get from x-forwared-for if it set (behind reverse proxy)
182+
return req.headers['x-forwarded-for'].split(',')[0];
183+
} else if (req.connection && req.connection.remoteAddress) {
184+
// no proxy, try getting from connection.remoteAddress
185+
return req.connection.remoteAddress;
186+
} else if (req.socket) {
187+
// try to get it from req.socket
188+
return req.socket.remoteAddress;
189+
} else if (req.connection && req.connection.socket) {
190+
// try to get it form the connection.socket
191+
return req.connection.socket.remoteAddress;
192+
} else {
193+
// if non above, fallback.
194+
return req.ip;
195+
}
196+
}
197+
174198
function httpAuth(req) {
175199
if (!(req.req || req).headers.authorization)
176200
return ;

0 commit comments

Comments
 (0)