From 18072e1d8b5281db43ded8b21433ee177bae9dfa Mon Sep 17 00:00:00 2001 From: Julian Dominguez-Schatz Date: Tue, 24 Feb 2026 10:32:50 -0500 Subject: [PATCH] Validate file IDs for correctness (#7067) * Validate file IDs for correctness * Add release notes --- packages/sync-server/src/app-sync.test.ts | 23 +++++++++++++++++++++++ packages/sync-server/src/app-sync.ts | 13 +++++++++++++ upcoming-release-notes/7067.md | 6 ++++++ 3 files changed, 42 insertions(+) create mode 100644 upcoming-release-notes/7067.md diff --git a/packages/sync-server/src/app-sync.test.ts b/packages/sync-server/src/app-sync.test.ts index a31efb2751..12e75451f9 100644 --- a/packages/sync-server/src/app-sync.test.ts +++ b/packages/sync-server/src/app-sync.test.ts @@ -366,6 +366,19 @@ describe('/upload-user-file', () => { expect(res.text).toBe('fileId is required'); }); + it('returns 400 for invalid fileId format', async () => { + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-name', 'test-file') + .set('x-actual-file-id', 'budget@2026') + .send(Buffer.from('file content')); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('invalid fileId'); + }); + it('uploads a new file successfully', async () => { const fileId = crypto.randomBytes(16).toString('hex'); const fileName = 'test-file.txt'; @@ -670,6 +683,16 @@ describe('/download-user-file', () => { expect(res.text).toBe('User or file not found'); }); + it('returns 400 for invalid fileId format', async () => { + const res = await request(app) + .get('/download-user-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', 'budget@2026'); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('invalid fileId'); + }); + it('returns 500 error if the file does not exist on the filesystem', async () => { getAccountDb().mutate( 'INSERT INTO files (id, deleted) VALUES (?, FALSE)', diff --git a/packages/sync-server/src/app-sync.ts b/packages/sync-server/src/app-sync.ts index 78626002d7..38a8586f1c 100644 --- a/packages/sync-server/src/app-sync.ts +++ b/packages/sync-server/src/app-sync.ts @@ -49,11 +49,16 @@ app.use(express.json({ limit: `${config.get('upload.fileSizeLimitMB')}mb` })); export { app as handlers }; const OK_RESPONSE = { status: 'ok' }; +const FILE_ID_PATTERN = /^[A-Za-z0-9_-]+$/; function boolToInt(deleted) { return deleted ? 1 : 0; } +function isValidFileId(fileId: unknown): fileId is string { + return typeof fileId === 'string' && FILE_ID_PATTERN.test(fileId); +} + const verifyFileExists = (fileId, filesService, res, errorObject) => { try { return filesService.get(fileId); @@ -256,6 +261,10 @@ app.post('/upload-user-file', async (req, res) => { res.status(400).send('fileId is required'); return; } + if (!isValidFileId(fileId)) { + res.status(400).send('invalid fileId'); + return; + } let groupId = req.headers['x-actual-group-id'] || null; const encryptMeta = req.headers['x-actual-encrypt-meta'] || null; @@ -352,6 +361,10 @@ app.get('/download-user-file', async (req, res) => { res.status(400).send('Single file ID is required'); return; } + if (!isValidFileId(fileId)) { + res.status(400).send('invalid fileId'); + return; + } const filesService = new FilesService(getAccountDb()); const file = verifyFileExists( diff --git a/upcoming-release-notes/7067.md b/upcoming-release-notes/7067.md new file mode 100644 index 0000000000..d8d09e5ddd --- /dev/null +++ b/upcoming-release-notes/7067.md @@ -0,0 +1,6 @@ +--- +category: Bugfixes +authors: [jfdoming] +--- + +Validate file IDs for correctness