Skip to content

Commit edef824

Browse files
authored
feat: Enforce remote access restrictions on agent endpoint (#3255)
1 parent f0ea977 commit edef824

File tree

2 files changed

+233
-9
lines changed

2 files changed

+233
-9
lines changed

Parse-Dashboard/app.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ module.exports = function(config, options) {
8787
cookieSessionStore: options.cookieSessionStore
8888
});
8989

90+
/**
91+
* Checks whether a request is from localhost.
92+
*/
93+
function isLocalRequest(req) {
94+
return req.connection.remoteAddress === '127.0.0.1' ||
95+
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
96+
req.connection.remoteAddress === '::1';
97+
}
98+
99+
/**
100+
* Middleware that enforces remote access restrictions:
101+
* - Requires HTTPS for remote requests (unless allowInsecureHTTP is set)
102+
* - Requires users to be configured for remote access (unless dev mode is enabled)
103+
*/
104+
function enforceRemoteAccessRestrictions(req, res, next) {
105+
if (!options.dev && !isLocalRequest(req)) {
106+
if (!req.secure && !options.allowInsecureHTTP) {
107+
return res.status(403).json({ error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
108+
}
109+
if (!users) {
110+
return res.status(401).json({ error: 'Configure a user to access Parse Dashboard remotely' });
111+
}
112+
}
113+
next();
114+
}
115+
90116
// CSRF error handler
91117
app.use(function (err, req, res, next) {
92118
if (err.code !== 'EBADCSRFTOKEN') {return next(err)}
@@ -109,13 +135,7 @@ module.exports = function(config, options) {
109135
agent: config.agent,
110136
};
111137

112-
//Based on advice from Doug Wilson here:
113-
//https://github.com/expressjs/express/issues/2518
114-
const requestIsLocal =
115-
req.connection.remoteAddress === '127.0.0.1' ||
116-
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
117-
req.connection.remoteAddress === '::1';
118-
if (!options.dev && !requestIsLocal) {
138+
if (!options.dev && !isLocalRequest(req)) {
119139
if (!req.secure && !options.allowInsecureHTTP) {
120140
//Disallow HTTP requests except on localhost, to prevent the master key from being transmitted in cleartext
121141
return res.send({ success: false, error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
@@ -179,7 +199,7 @@ module.exports = function(config, options) {
179199

180200
//They didn't provide auth, and have configured the dashboard to not need auth
181201
//(ie. didn't supply usernames and passwords)
182-
if (requestIsLocal || options.dev) {
202+
if (isLocalRequest(req) || options.dev) {
183203
//Allow no-auth access on localhost only, if they have configured the dashboard to not need auth
184204
await Promise.all(
185205
response.apps.map(async (app) => {
@@ -329,8 +349,9 @@ module.exports = function(config, options) {
329349
}
330350
}
331351

332-
// Agent API endpoint — middleware chain: auth check (401) → CSRF validation (403) → handler
352+
// Agent API endpoint — middleware chain: remote access guard → auth check (401) → CSRF validation (403) → handler
333353
app.post('/apps/:appId/agent',
354+
enforceRemoteAccessRestrictions,
334355
(req, res, next) => {
335356
if (users && (!req.user || !req.user.isAuthenticated)) {
336357
return res.status(401).json({ error: 'Unauthorized' });

src/lib/tests/AgentAuth.test.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,206 @@ describe('Agent endpoint security', () => {
373373
expect(res.status).not.toBe(403);
374374
});
375375
});
376+
377+
// ---------------------------------------------------------------
378+
// No-user mode — remote access guard
379+
// ---------------------------------------------------------------
380+
381+
describe('Agent endpoint no-user mode', () => {
382+
let server;
383+
let port;
384+
385+
const noUserConfig = {
386+
apps: [
387+
{
388+
serverURL: 'http://localhost:1337/parse',
389+
appId: 'testAppId',
390+
masterKey: 'testMasterKey',
391+
appName: 'TestApp',
392+
},
393+
],
394+
// No users configured
395+
agent: {
396+
models: [
397+
{
398+
name: 'test-model',
399+
provider: 'openai',
400+
model: 'gpt-4',
401+
apiKey: 'fake-api-key-for-testing',
402+
},
403+
],
404+
},
405+
};
406+
407+
beforeAll((done) => {
408+
const parseDashboard = require('../../../Parse-Dashboard/app.js');
409+
// dev: false to enable the remote access guard
410+
const dashboardApp = parseDashboard(noUserConfig, {
411+
cookieSessionSecret: SESSION_SECRET,
412+
});
413+
414+
const parentApp = express();
415+
parentApp.use('/', dashboardApp);
416+
417+
server = parentApp.listen(0, () => {
418+
port = server.address().port;
419+
done();
420+
});
421+
});
422+
423+
afterAll((done) => {
424+
if (server) {
425+
server.close(done);
426+
} else {
427+
done();
428+
}
429+
});
430+
431+
it('allows local requests to the agent endpoint in no-user mode', async () => {
432+
// Requests to 127.0.0.1 are local, so they should pass the guard
433+
const res = await makeRequest(port, {
434+
method: 'POST',
435+
path: '/apps/TestApp/agent',
436+
body: agentBody(),
437+
});
438+
// Should not be blocked by the no-user guard (may fail at CSRF or later)
439+
expect(res.status).not.toBe(401);
440+
});
441+
});
442+
443+
describe('Agent endpoint no-user mode — remote requests', () => {
444+
let server;
445+
let port;
446+
447+
const noUserConfig = {
448+
apps: [
449+
{
450+
serverURL: 'http://localhost:1337/parse',
451+
appId: 'testAppId',
452+
masterKey: 'testMasterKey',
453+
appName: 'TestApp',
454+
},
455+
],
456+
agent: {
457+
models: [
458+
{
459+
name: 'test-model',
460+
provider: 'openai',
461+
model: 'gpt-4',
462+
apiKey: 'fake-api-key-for-testing',
463+
},
464+
],
465+
},
466+
};
467+
468+
beforeAll((done) => {
469+
const parseDashboard = require('../../../Parse-Dashboard/app.js');
470+
const dashboardApp = parseDashboard(noUserConfig, {
471+
cookieSessionSecret: SESSION_SECRET,
472+
});
473+
474+
const parentApp = express();
475+
// Spoof a non-local remote address before the dashboard middleware
476+
parentApp.use((req, _res, next) => {
477+
Object.defineProperty(req.connection, 'remoteAddress', {
478+
value: '203.0.113.1',
479+
writable: true,
480+
configurable: true,
481+
});
482+
next();
483+
});
484+
parentApp.use('/', dashboardApp);
485+
486+
server = parentApp.listen(0, () => {
487+
port = server.address().port;
488+
done();
489+
});
490+
});
491+
492+
afterAll((done) => {
493+
if (server) {
494+
server.close(done);
495+
} else {
496+
done();
497+
}
498+
});
499+
500+
it('returns 403 for remote non-HTTPS requests to the agent endpoint', async () => {
501+
const res = await makeRequest(port, {
502+
method: 'POST',
503+
path: '/apps/TestApp/agent',
504+
body: agentBody(),
505+
});
506+
expect(res.status).toBe(403);
507+
expect(res.body.error).toBe('Parse Dashboard can only be remotely accessed via HTTPS');
508+
});
509+
});
510+
511+
describe('Agent endpoint no-user mode — remote requests with allowInsecureHTTP', () => {
512+
let server;
513+
let port;
514+
515+
const noUserConfig = {
516+
apps: [
517+
{
518+
serverURL: 'http://localhost:1337/parse',
519+
appId: 'testAppId',
520+
masterKey: 'testMasterKey',
521+
appName: 'TestApp',
522+
},
523+
],
524+
agent: {
525+
models: [
526+
{
527+
name: 'test-model',
528+
provider: 'openai',
529+
model: 'gpt-4',
530+
apiKey: 'fake-api-key-for-testing',
531+
},
532+
],
533+
},
534+
};
535+
536+
beforeAll((done) => {
537+
const parseDashboard = require('../../../Parse-Dashboard/app.js');
538+
const dashboardApp = parseDashboard(noUserConfig, {
539+
cookieSessionSecret: SESSION_SECRET,
540+
allowInsecureHTTP: true,
541+
});
542+
543+
const parentApp = express();
544+
// Spoof a non-local remote address before the dashboard middleware
545+
parentApp.use((req, _res, next) => {
546+
Object.defineProperty(req.connection, 'remoteAddress', {
547+
value: '203.0.113.1',
548+
writable: true,
549+
configurable: true,
550+
});
551+
next();
552+
});
553+
parentApp.use('/', dashboardApp);
554+
555+
server = parentApp.listen(0, () => {
556+
port = server.address().port;
557+
done();
558+
});
559+
});
560+
561+
afterAll((done) => {
562+
if (server) {
563+
server.close(done);
564+
} else {
565+
done();
566+
}
567+
});
568+
569+
it('returns 401 for remote requests to the agent endpoint in no-user mode when HTTPS is bypassed', async () => {
570+
const res = await makeRequest(port, {
571+
method: 'POST',
572+
path: '/apps/TestApp/agent',
573+
body: agentBody(),
574+
});
575+
expect(res.status).toBe(401);
576+
expect(res.body.error).toBe('Configure a user to access Parse Dashboard remotely');
577+
});
578+
});

0 commit comments

Comments
 (0)