VYPR
Medium severity6.5NVD Advisory· Published Mar 9, 2026· Updated Apr 9, 2026

CVE-2026-3089

CVE-2026-3089

Description

Actual Sync Server allows authenticated users to upload files through POST /sync/upload-user-file. In versions prior to 26.3.0, improper validation of the user-controlled x-actual-file-id header means that traversal segments (../) can escape the intended directory and write files outside userFiles.This issue affects prior versions of Actual Sync Server 26.3.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@actual-app/sync-servernpm
< 26.3.026.3.0

Affected products

1

Patches

1
18072e1d8b52

Validate file IDs for correctness (#7067)

https://github.com/actualbudget/actualJulian Dominguez-SchatzFeb 24, 2026via ghsa
3 files changed · +42 0
  • packages/sync-server/src/app-sync.test.ts+23 0 modified
    @@ -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)',
    
  • packages/sync-server/src/app-sync.ts+13 0 modified
    @@ -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(
    
  • upcoming-release-notes/7067.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +category: Bugfixes
    +authors: [jfdoming]
    +---
    +
    +Validate file IDs for correctness
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

50