diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 78e30566867d..ac1329e85098 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -11,7 +11,7 @@ import { App } from "../app" import { AuthType, DefaultedArgs } from "../cli" import { commit, rootPath } from "../constants" import { Heart } from "../heart" -import { redirect } from "../http" +import { ensureAuthenticated, redirect } from "../http" import { CoderSettings, SettingsProvider } from "../settings" import { UpdateProvider } from "../update" import { getMediaMime, paths } from "../util" @@ -113,7 +113,7 @@ export const register = async ( app.router.all("/proxy/:port{/*path}", async (req, res) => { await pathProxy.proxy(req, res) }) - app.wsRouter.get("/proxy/:port{/*path}", async (req) => { + app.wsRouter.get("/proxy/:port{/*path}", ensureAuthenticated, async (req) => { await pathProxy.wsProxy(req as unknown as WebsocketRequest) }) // These two routes pass through the path directly. @@ -125,7 +125,7 @@ export const register = async ( proxyBasePath: args["abs-proxy-base-path"], }) }) - app.wsRouter.get("/absproxy/:port{/*path}", async (req) => { + app.wsRouter.get("/absproxy/:port{/*path}", ensureAuthenticated, async (req) => { await pathProxy.wsProxy(req as unknown as WebsocketRequest, { passthroughPath: true, proxyBasePath: args["abs-proxy-base-path"], diff --git a/test/invariant_index.test.ts b/test/invariant_index.test.ts new file mode 100644 index 000000000000..82984df5a71e --- /dev/null +++ b/test/invariant_index.test.ts @@ -0,0 +1,32 @@ +import { app } from '../../src/node/routes/index'; +import request from 'supertest'; + +describe('Protected endpoints reject unauthenticated requests', () => { + const endpoints = [ + '/proxy/8080', + '/absproxy/3000' + ]; + + const payloads = [ + { description: 'missing authorization header', headers: {} }, + { description: 'malformed token', headers: { Authorization: 'Bearer invalid.token.here' } }, + { description: 'expired token', headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzMwNzYwMDB9.invalid-signature' } }, + { description: 'valid token', headers: { Authorization: 'Bearer valid.test.token' } } + ]; + + test.each(endpoints)('endpoint %s rejects unauthenticated requests', async (endpoint) => { + test.each(payloads)('with $description', async ({ headers }) => { + const response = await request(app) + .get(endpoint) + .set(headers); + + // Should reject with 401 or 403 for all but valid token case + if (headers.Authorization === 'Bearer valid.test.token') { + expect(response.status).not.toBe(401); + expect(response.status).not.toBe(403); + } else { + expect([401, 403]).toContain(response.status); + } + }); + }); +}); \ No newline at end of file