From a62fba5dadecf7d79c1ab24d8a96948f45dd19d6 Mon Sep 17 00:00:00 2001 From: trangiasang77 Date: Sat, 30 May 2026 20:53:26 +0700 Subject: [PATCH 1/3] fix(bash): normalize allowlist path segments --- .../BuiltInToolService/tools/bash/helper.ts | 31 ++++++++++++++++++- .../tools/bash/helper.test.ts | 15 +++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts index 81cf4e6d..0c58d336 100644 --- a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts +++ b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts @@ -16,7 +16,36 @@ import { } from './constants'; function normalizeDirectoryPath(path: string): string { - return path.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase(); + const normalizedSeparators = path.trim().replace(/\//g, '\\').replace(/\\+$/, ''); + const driveMatch = /^([a-zA-Z]:)(?:\\(.*))?$/.exec(normalizedSeparators); + const hasRoot = normalizedSeparators.startsWith('\\'); + const prefix = driveMatch?.[1]?.toLowerCase() ?? (hasRoot ? '\\' : ''); + const body = driveMatch ? (driveMatch[2] ?? '') : normalizedSeparators.replace(/^\\+/, ''); + const resolvedSegments: string[] = []; + + for (const segment of body.split(/\\+/)) { + if (!segment || segment === '.') { + continue; + } + + if (segment === '..') { + if (resolvedSegments.length > 0) { + resolvedSegments.pop(); + } else if (!prefix) { + resolvedSegments.push(segment); + } + continue; + } + + resolvedSegments.push(segment.toLowerCase()); + } + + const normalizedBody = resolvedSegments.join('\\'); + if (!prefix) { + return normalizedBody; + } + + return normalizedBody ? `${prefix}\\${normalizedBody}` : prefix; } function isWithinAllowedDirectory(path: string, allowlist: string[]): boolean { diff --git a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts index 15741d99..093a454a 100644 --- a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts +++ b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts @@ -94,6 +94,21 @@ describe('resolveCommandContext', () => { ).rejects.toThrow('Working directory is outside the allowed scope'); }); + it('rejects working directories that escape the allowlist with parent segments', async () => { + setLocale('en-US'); + const config = { + ...baseConfig, + allowedWorkingDirectories: ['D:/allowed'], + }; + + await expect( + resolveCommandContext( + { command: 'dir', workingDirectory: 'D:/allowed/../other' }, + config + ) + ).rejects.toThrow('Working directory is outside the allowed scope'); + }); + it('accepts command inside allowed directories', async () => { const config = { ...baseConfig, From a4a534d983a63f9a23d764c3fd052de9cda99cea Mon Sep 17 00:00:00 2001 From: trangiasang77 Date: Sun, 7 Jun 2026 09:28:53 +0700 Subject: [PATCH 2/3] fix(bash): preserve consecutive relative parent segments --- .../services/BuiltInToolService/tools/bash/helper.ts | 3 ++- .../BuiltInToolService/tools/bash/helper.test.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts index 0c58d336..f103ce9d 100644 --- a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts +++ b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts @@ -29,7 +29,8 @@ function normalizeDirectoryPath(path: string): string { } if (segment === '..') { - if (resolvedSegments.length > 0) { + const lastSegment = resolvedSegments[resolvedSegments.length - 1]; + if (lastSegment && lastSegment !== '..') { resolvedSegments.pop(); } else if (!prefix) { resolvedSegments.push(segment); diff --git a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts index 093a454a..b14c4eb3 100644 --- a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts +++ b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts @@ -109,6 +109,18 @@ describe('resolveCommandContext', () => { ).rejects.toThrow('Working directory is outside the allowed scope'); }); + it('rejects relative working directories that escape with consecutive parent segments', async () => { + setLocale('en-US'); + const config = { + ...baseConfig, + allowedWorkingDirectories: ['foo'], + }; + + await expect( + resolveCommandContext({ command: 'dir', workingDirectory: '../../foo' }, config) + ).rejects.toThrow('Working directory is outside the allowed scope'); + }); + it('accepts command inside allowed directories', async () => { const config = { ...baseConfig, From 7088f8303bfa35341d7a2fa9a55aca6480489d5c Mon Sep 17 00:00:00 2001 From: ThunderTr77 Date: Thu, 11 Jun 2026 20:17:31 +0700 Subject: [PATCH 3/3] fix(bash): preserve UNC share roots in allowlist checks --- .../BuiltInToolService/tools/bash/helper.ts | 18 +++++++++++++++--- .../tools/bash/helper.test.ts | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts index f103ce9d..2eb8cb77 100644 --- a/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts +++ b/apps/desktop/src/services/BuiltInToolService/tools/bash/helper.ts @@ -17,10 +17,18 @@ import { function normalizeDirectoryPath(path: string): string { const normalizedSeparators = path.trim().replace(/\//g, '\\').replace(/\\+$/, ''); + const uncMatch = /^\\\\([^\\]+)\\([^\\]+)(?:\\(.*))?$/.exec(normalizedSeparators); const driveMatch = /^([a-zA-Z]:)(?:\\(.*))?$/.exec(normalizedSeparators); const hasRoot = normalizedSeparators.startsWith('\\'); - const prefix = driveMatch?.[1]?.toLowerCase() ?? (hasRoot ? '\\' : ''); - const body = driveMatch ? (driveMatch[2] ?? '') : normalizedSeparators.replace(/^\\+/, ''); + const prefix = + uncMatch && uncMatch[1] && uncMatch[2] + ? `\\\\${uncMatch[1].toLowerCase()}\\${uncMatch[2].toLowerCase()}` + : (driveMatch?.[1]?.toLowerCase() ?? (hasRoot ? '\\' : '')); + const body = uncMatch + ? (uncMatch[3] ?? '') + : driveMatch + ? (driveMatch[2] ?? '') + : normalizedSeparators.replace(/^\\+/, ''); const resolvedSegments: string[] = []; for (const segment of body.split(/\\+/)) { @@ -46,7 +54,11 @@ function normalizeDirectoryPath(path: string): string { return normalizedBody; } - return normalizedBody ? `${prefix}\\${normalizedBody}` : prefix; + if (!normalizedBody) { + return prefix; + } + + return prefix.endsWith('\\') ? `${prefix}${normalizedBody}` : `${prefix}\\${normalizedBody}`; } function isWithinAllowedDirectory(path: string, allowlist: string[]): boolean { diff --git a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts index b14c4eb3..f9b5c29c 100644 --- a/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts +++ b/apps/desktop/tests/services/BuiltInToolService/tools/bash/helper.test.ts @@ -121,6 +121,21 @@ describe('resolveCommandContext', () => { ).rejects.toThrow('Working directory is outside the allowed scope'); }); + it('rejects UNC paths that escape to a sibling share before re-entering the allowlist', async () => { + setLocale('en-US'); + const config = { + ...baseConfig, + allowedWorkingDirectories: ['\\\\server\\share'], + }; + + await expect( + resolveCommandContext( + { command: 'dir', workingDirectory: '\\\\server\\other\\..\\share\\secret' }, + config + ) + ).rejects.toThrow('Working directory is outside the allowed scope'); + }); + it('accepts command inside allowed directories', async () => { const config = { ...baseConfig,