From 326f928a3416cf1983f869e5a9d5401d41592b10 Mon Sep 17 00:00:00 2001 From: alexcibotari Date: Mon, 26 Oct 2026 11:15:24 +0100 Subject: [PATCH] fix: stop exposing space API tokens from the migration API Problem: GET /api/migration/spaces/:spaceId/tokens returned every space API token document. The self-hosted Firebase import no longer imports space tokens, so the route had no consumer, and it let anyone holding the migration token read credentials for every space. Fix: drop 'tokens' from the whole-collection routes; the path now falls through to a 404. Test asserts the route is gone and leaks no token. --- functions/src/migration.test.ts | 7 ++++++- functions/src/migration.ts | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/functions/src/migration.test.ts b/functions/src/migration.test.ts index a0e6b8901..cef7f958a 100644 --- a/functions/src/migration.test.ts +++ b/functions/src/migration.test.ts @@ -82,11 +82,16 @@ describe('migration API', () => { }); it('returns small collections whole, with ids', async () => { - expect((await get('/api/migration/spaces/s1/tokens')).body).toEqual([{ id: 'TOKEN000000000000001', name: 'web', createdAt: '2025-01-02T00:00:00.000Z' }]); expect((await get('/api/migration/spaces/s1/schemas')).body).toEqual([{ id: 'page', type: 'ROOT', fields: [] }]); expect((await get('/api/migration/spaces/s1/webhooks')).body).toEqual([]); }); + it('does not expose space API tokens', async () => { + const res = await get('/api/migration/spaces/s1/tokens'); + expect(res.status).toBe(404); + expect(res.text).not.toContain('TOKEN000000000000001'); + }); + it('pages large collections by 500, cursor = last id, null at the end', async () => { const first = (await get('/api/migration/spaces/s1/translations')).body; expect(first.items).toHaveLength(500); diff --git a/functions/src/migration.ts b/functions/src/migration.ts index 7271be622..9ec896b03 100644 --- a/functions/src/migration.ts +++ b/functions/src/migration.ts @@ -81,7 +81,9 @@ MIGRATION.get('/api/migration/spaces/:spaceId', async (req, res) => { res.json(toJson({ id: doc.id, data: () => doc.data() })); }); -for (const name of ['tokens', 'webhooks', 'schemas']) { +// Space API tokens are deliberately absent: the self-hosted import does not carry them over, and +// they are credentials, so the migration token should not be able to read them. +for (const name of ['webhooks', 'schemas']) { MIGRATION.get(`/api/migration/spaces/:spaceId/${name}`, async (req, res) => { if (!(await requireSpace(req, res))) return; const snapshot = await firestoreService.collection(`spaces/${req.params['spaceId']}/${name}`).orderBy('__name__').get();