diff --git a/src/backend/services/fs/FSService.test.ts b/src/backend/services/fs/FSService.test.ts index fa75ce870..f346ffb6e 100644 --- a/src/backend/services/fs/FSService.test.ts +++ b/src/backend/services/fs/FSService.test.ts @@ -3367,6 +3367,26 @@ describe('FSService permission rules', () => { expect(error.legacyCode).toBe('subject_does_not_exist'); }); + // The old raw `split('fs:')` dropped the mode and stripped the trailing `fs`, resolving this to a bare `fs:` that subsumes every mode. + it('does not let an embedded fs: escalate a scoped grant to a bare one', async () => { + const error = await caught(() => + server.services.permission.rewritePermission( + `fs:${user.home}fs:junk:read`, + ), + ); + expect(error.statusCode).toBe(404); + expect(error.legacyCode).toBe('subject_does_not_exist'); + }); + + it('keeps the mode when a later component contains fs:', async () => { + // `fs` in the mode position is data, not a delimiter: the path resolves and the mode is preserved, never collapsed to bare. + await expect( + server.services.permission.rewritePermission( + `fs:${file.path}:fs:read`, + ), + ).resolves.toBe(`fs:${file.uuid}:fs:read`); + }); + it('leaves uuid-addressed and non-fs permissions untouched', async () => { await expect( server.services.permission.rewritePermission( diff --git a/src/backend/services/fs/FSService.ts b/src/backend/services/fs/FSService.ts index 3c1efb108..b48052ab1 100644 --- a/src/backend/services/fs/FSService.ts +++ b/src/backend/services/fs/FSService.ts @@ -182,14 +182,18 @@ export class FSService extends PuterService { !permission.startsWith(`${MANAGE_PERM_PREFIX}:fs:`) ) return false; - const [, specifier] = permission.split('fs:'); + // Parse on component boundaries: a path can contain `fs:` (e.g. a home dir named `…fs`), which a raw split mistakes for the mode delimiter. + const parts = PermissionUtil.split(permission); + const fsIndex = parts[0] === MANAGE_PERM_PREFIX ? 1 : 0; + const specifier = parts[fsIndex + 1]; return Boolean(specifier && specifier.startsWith('/')); }, rewrite: async (permission: string): Promise => { - const [manageOpt, pathPerm] = permission.split('fs:'); - const parts = PermissionUtil.split(pathPerm); - const path = parts[0]; - const rest = parts.slice(1); + const parts = PermissionUtil.split(permission); + const hasManage = parts[0] === MANAGE_PERM_PREFIX; + const fsIndex = hasManage ? 1 : 0; + const path = parts[fsIndex + 1]; + const rest = parts.slice(fsIndex + 2); if (!path) return permission; const entry = await fsEntryStore.getEntryByPath(path); if (!entry) { @@ -197,9 +201,8 @@ export class FSService extends PuterService { legacyCode: 'subject_does_not_exist', }); } - const manage = manageOpt.replace(':', ''); const joined = PermissionUtil.join('fs', entry.uuid, ...rest); - return manage ? `${manage}:${joined}` : joined; + return hasManage ? `${MANAGE_PERM_PREFIX}:${joined}` : joined; }, });