diff --git a/src/routes/irma/[...path]/+server.ts b/src/routes/irma/[...path]/+server.ts index 649bf80..0324fc9 100644 --- a/src/routes/irma/[...path]/+server.ts +++ b/src/routes/irma/[...path]/+server.ts @@ -1,7 +1,13 @@ +import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { YIVI_SERVER_URL, YIVI_SERVER_TOKEN } from '$lib/server/auth/yivi'; +import { isAllowed } from './allowlist'; const handler: RequestHandler = async ({ params, request }) => { + if (!isAllowed(params.path)) { + error(403, 'Forbidden'); + } + const url = `${YIVI_SERVER_URL}/${params.path}`; const headers: Record = {}; diff --git a/src/routes/irma/[...path]/allowlist.ts b/src/routes/irma/[...path]/allowlist.ts new file mode 100644 index 0000000..1b249ab --- /dev/null +++ b/src/routes/irma/[...path]/allowlist.ts @@ -0,0 +1,13 @@ +// The Yivi frontend SDK only talks to /irma/session[...]. Everything else is +// either administrative (e.g. /scheme, /configuration) or unknown territory +// on the upstream Yivi server and must not be proxied with the server's +// auth token. +export const ALLOWED_PREFIXES = ['session']; + +export function isAllowed(path: string | undefined): boolean { + if (!path) return false; + // Reject path traversal attempts outright. + if (path.includes('..')) return false; + const firstSegment = path.split('/')[0]; + return ALLOWED_PREFIXES.includes(firstSegment); +} diff --git a/tests/unit/irma-proxy-allowlist.test.ts b/tests/unit/irma-proxy-allowlist.test.ts new file mode 100644 index 0000000..121eca2 --- /dev/null +++ b/tests/unit/irma-proxy-allowlist.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { isAllowed } from '../../src/routes/irma/[...path]/allowlist'; + +describe('irma proxy path allowlist', () => { + it('allows the session prefix', () => { + expect(isAllowed('session')).toBe(true); + }); + + it('allows session subpaths', () => { + expect(isAllowed('session/abc123')).toBe(true); + expect(isAllowed('session/abc123/status')).toBe(true); + }); + + it('rejects unknown top-level prefixes', () => { + expect(isAllowed('admin')).toBe(false); + expect(isAllowed('scheme')).toBe(false); + expect(isAllowed('configuration')).toBe(false); + expect(isAllowed('keyshare')).toBe(false); + }); + + it('rejects path traversal attempts', () => { + expect(isAllowed('session/../admin')).toBe(false); + expect(isAllowed('../admin')).toBe(false); + expect(isAllowed('session/..')).toBe(false); + }); + + it('rejects empty and undefined paths', () => { + expect(isAllowed('')).toBe(false); + expect(isAllowed(undefined)).toBe(false); + }); + + it('does not allow prefix-spoofing like sessionadmin', () => { + expect(isAllowed('sessionadmin')).toBe(false); + expect(isAllowed('sessionfoo/bar')).toBe(false); + }); +});