Skip to content

Commit e016d81

Browse files
authored
fix: The client IP address may be determined incorrectly in some cases; it is now required to set the Parse Server option trustProxy accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting; this fixes a security vulnerability in which the Parse Server option masterKeyIps may be circumvented, see [GHSA-vm5r-c87r-pf6x](GHSA-vm5r-c87r-pf6x) (#8369)
1 parent c8bc200 commit e016d81

File tree

6 files changed

+18
-134
lines changed

6 files changed

+18
-134
lines changed

spec/Middlewares.spec.js

+3-118
Original file line numberDiff line numberDiff line change
@@ -158,78 +158,6 @@ describe('middlewares', () => {
158158
});
159159
});
160160

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-
233161
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
234162
AppCache.put(fakeReq.body._ApplicationId, {
235163
masterKey: 'masterKey',
@@ -243,52 +171,9 @@ describe('middlewares', () => {
243171
});
244172
});
245173

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);
174+
it('can set trust proxy', async () => {
175+
const server = await reconfigureServer({ trustProxy: 1 });
176+
expect(server.app.parent.settings['trust proxy']).toBe(1);
292177
});
293178

294179
it('should properly expose the headers', () => {

src/Options/Definitions.js

+7
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,13 @@ module.exports.ParseServerOptions = {
477477
help: 'Starts the liveQuery server',
478478
action: parsers.booleanParser,
479479
},
480+
trustProxy: {
481+
env: 'PARSE_SERVER_TRUST_PROXY',
482+
help:
483+
'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.',
484+
action: parsers.objectParser,
485+
default: [],
486+
},
480487
userSensitiveFields: {
481488
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
482489
help:

src/Options/docs.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
8989
* @property {Boolean} silent Disables console output
9090
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
91+
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
9192
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
9293
* @property {Boolean} verbose Set the logging to verbose
9394
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.

src/Options/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ export interface ParseServerOptions {
238238
cluster: ?NumberOrBoolean;
239239
/* middleware for express server, can be string or function */
240240
middleware: ?((() => void) | string);
241+
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
242+
:DEFAULT: false */
243+
trustProxy: ?any;
241244
/* Starts the liveQuery server */
242245
startLiveQueryServer: ?boolean;
243246
/* Live query server configuration options (will start the liveQuery server) */

src/ParseServer.js

+3
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ class ParseServer {
304304
options
305305
);
306306
}
307+
if (options.trustProxy) {
308+
app.set('trust proxy', options.trustProxy);
309+
}
307310
/* istanbul ignore next */
308311
if (!process.env.TESTING) {
309312
configureListeners(this);

src/middlewares.js

+1-16
Original file line numberDiff line numberDiff line change
@@ -280,22 +280,7 @@ export function handleParseHeaders(req, res, next) {
280280
}
281281

282282
function getClientIp(req) {
283-
if (req.headers['x-forwarded-for']) {
284-
// try to get from x-forwared-for if it set (behind reverse proxy)
285-
return req.headers['x-forwarded-for'].split(',')[0];
286-
} else if (req.connection && req.connection.remoteAddress) {
287-
// no proxy, try getting from connection.remoteAddress
288-
return req.connection.remoteAddress;
289-
} else if (req.socket) {
290-
// try to get it from req.socket
291-
return req.socket.remoteAddress;
292-
} else if (req.connection && req.connection.socket) {
293-
// try to get it form the connection.socket
294-
return req.connection.socket.remoteAddress;
295-
} else {
296-
// if non above, fallback.
297-
return req.ip;
298-
}
283+
return req.ip;
299284
}
300285

301286
function httpAuth(req) {

0 commit comments

Comments
 (0)