diff --git a/CHANGELOG.md b/CHANGELOG.md index ac490d64ab..5a56659b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to ### Fixed +- 🐛(y-provider) send X-Forwarded-Proto on internal backend calls #2541 - 🐛(frontend) redirect homepage to login when homepage feat is disabled #2521 ### Changed diff --git a/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts b/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts index 17c88cf0be..411167df27 100644 --- a/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts +++ b/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts @@ -63,4 +63,29 @@ describe('CollaborationBackend', () => { axiosGetSpy.mockRestore(); }); + + test('always sends X-Forwarded-Proto: https regardless of the incoming header', async () => { + const axiosGetSpy = vi.spyOn(axios, 'get').mockResolvedValue({ + status: 200, + data: { id: 'test-user-id', email: 'test@example.com' }, + }); + + const { fetchCurrentUser } = await import('@/api/collaborationBackend'); + + await fetchCurrentUser({ + cookie: 'test-cookie', + 'x-forwarded-proto': 'http', + }); + + expect(axiosGetSpy).toHaveBeenCalledWith( + 'http://app-dev:8000/api/v1.0/users/me/', + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Forwarded-Proto': 'https', + }), + }), + ); + + axiosGetSpy.mockRestore(); + }); }); diff --git a/src/frontend/servers/y-provider/src/api/collaborationBackend.ts b/src/frontend/servers/y-provider/src/api/collaborationBackend.ts index a9ae76b247..b10bb754ca 100644 --- a/src/frontend/servers/y-provider/src/api/collaborationBackend.ts +++ b/src/frontend/servers/y-provider/src/api/collaborationBackend.ts @@ -52,6 +52,19 @@ interface Doc { }; } +/** + * In production the backend sets SECURE_SSL_REDIRECT and only trusts + * X-Forwarded-Proto to tell whether TLS was terminated upstream. This call is + * internal, so when COLLABORATION_BACKEND_BASE_URL points at the backend + * directly it never crosses the reverse proxy: nothing sets the header, Django + * answers a 301 to https://:, and the client then speaks TLS to a + * port that serves plain HTTP. + * + * Always send https: it is the safe default given the current Production + * settings. Relaying the client's own X-Forwarded-Proto would let a client + * that sends X-Forwarded-Proto: http on its WebSocket upgrade force a 301 and + * break its own connection — a vector this fix has no reason to introduce. + */ async function fetch( path: string, requestHeaders: IncomingHttpHeaders, @@ -63,6 +76,7 @@ async function fetch( cookie: requestHeaders['cookie'], origin: requestHeaders['origin'], 'X-Y-Provider-Key': Y_PROVIDER_API_KEY, + 'X-Forwarded-Proto': 'https', }, }, );