diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts index 8becdfe59d..fd63806fc8 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.test.ts @@ -37,5 +37,23 @@ describe('startLoopbackServer', () => { expect(response.status).toBe(204) expect(response.headers.get('Access-Control-Allow-Private-Network')).toBe('true') + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://accounts.airi.build') + }) + + it('does not allow private-network preflight from untrusted origins', async () => { + const server = await startLoopbackServer() + servers.push(server) + + const response = await fetch(`http://127.0.0.1:${server.port}/callback`, { + method: 'OPTIONS', + headers: { + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Private-Network': 'true', + 'Origin': 'https://example.invalid', + }, + }) + + expect(response.headers.get('Access-Control-Allow-Private-Network')).toBeNull() + expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull() }) }) diff --git a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts index e9497a5443..c6005a5b1a 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/http-server/http/auth/index.ts @@ -1,4 +1,6 @@ -import { eventHandler, getQuery, H3, handleCors } from 'h3' +import type { CorsOptions } from 'h3' + +import { eventHandler, getQuery, getRequestHeader, H3, handleCors } from 'h3' import { createH3Server } from '../../server' @@ -7,6 +9,8 @@ export interface LoopbackCallbackResult { state: string } +const AIRI_AUTH_ORIGIN = 'https://accounts.airi.build' + /** * Starts a temporary loopback callback server for the Electron OIDC flow. * @@ -39,16 +43,18 @@ export async function startLoopbackServer(): Promise<{ const app = new H3() const loopbackServer = createH3Server({ app, host }) - const corsOptions = { - origin: '*', + const corsOptions: CorsOptions = { + origin: [AIRI_AUTH_ORIGIN], methods: '*', preflight: { statusCode: 204, }, - } as const + } + + function appendPrivateNetworkAccessHeaders(event: Parameters[0], response: T) { + if (getRequestHeader(event, 'origin') === AIRI_AUTH_ORIGIN) + response.headers.set('Access-Control-Allow-Private-Network', 'true') - function appendPrivateNetworkAccessHeaders(response: T) { - response.headers.set('Access-Control-Allow-Private-Network', 'true') return response } @@ -69,16 +75,16 @@ export async function startLoopbackServer(): Promise<{ app.options('/callback', eventHandler(async (event) => { const corsResponse = handleCors(event, corsOptions) if (corsResponse !== false) { - return appendPrivateNetworkAccessHeaders(corsResponse) + return appendPrivateNetworkAccessHeaders(event, corsResponse) } - return appendPrivateNetworkAccessHeaders(new Response(null, { status: 204 })) + return new Response(null, { status: 204 }) })) app.get('/callback', eventHandler(async (event) => { const corsResponse = handleCors(event, corsOptions) if (corsResponse !== false) { - return appendPrivateNetworkAccessHeaders(corsResponse) + return appendPrivateNetworkAccessHeaders(event, corsResponse) } const query = getQuery(event)