Skip to content

Commit b33353e

Browse files
authored
Ignore file-like domain suffixes in terminal sandbox (#307780)
* Ignore file-like domain suffixes in terminal sandbox * Handle file-like suffixes for URL and SSH domains
1 parent a240f2a commit b33353e

File tree

2 files changed

+72
-4
lines changed

2 files changed

+72
-4
lines changed

src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
135135
private static readonly _urlRegex = /(?:https?|wss?):\/\/[^\s'"`|&;<>]+/gi;
136136
private static readonly _sshRemoteRegex = /(?:^|[\s'"`])(?:[^\s@:'"`]+@)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(?::[^\s'"`|&;<>]+)(?=$|[\s'"`|&;<>])/gi;
137137
private static readonly _hostRegex = /(?:^|[\s'"`(=])([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(?::\d+)?(?=(?:\/[^\s'"`|&;<>]*)?(?:$|[\s'"`)\]|,;|&<>]))/gi;
138+
// While the regexes above are designed to minimize false positives, we may still pick up some file paths that look like domains. To reduce noise, maintain a list of common file extensions and filter out any domain candidates that end with these extensions.
139+
private static readonly _fileExtensionSuffixes = new Set([
140+
'7z', 'bz2', 'cjs', 'class', 'cpp', 'cs', 'css', 'csv', 'dll', 'exe', 'gif', 'gz', 'ico', 'jar',
141+
'env', 'java', 'jpeg', 'jpg', 'js', 'json', 'jsx', 'lock', 'log', 'md', 'mjs', 'pdf', 'php', 'png',
142+
'py', 'rar', 'rs', 'so', 'sql', 'svg', 'tar', 'tgz', 'toml', 'ts', 'tsx', 'txt', 'wasm', 'webp',
143+
'xml', 'yaml', 'yml', 'zip'
144+
]);
138145

139146
constructor(
140147
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -502,7 +509,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
502509

503510
TerminalSandboxService._sshRemoteRegex.lastIndex = 0;
504511
while ((match = TerminalSandboxService._sshRemoteRegex.exec(command)) !== null) {
505-
const domain = this._normalizeDomain(match[1]);
512+
const domain = this._normalizeDomain(match[1], true);
506513
if (domain) {
507514
domains.add(domain);
508515
}
@@ -522,13 +529,13 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
522529
private _extractDomainFromUrl(value: string): string | undefined {
523530
try {
524531
const authority = URI.parse(value).authority;
525-
return this._normalizeDomain(authority);
532+
return this._normalizeDomain(authority, true);
526533
} catch {
527534
return undefined;
528535
}
529536
}
530537

531-
private _normalizeDomain(value: string | undefined): string | undefined {
538+
private _normalizeDomain(value: string | undefined, fromUrl: boolean = false): string | undefined {
532539
if (!value) {
533540
return undefined;
534541
}
@@ -568,11 +575,19 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
568575
return undefined;
569576
}
570577

578+
// Disallow patterns that look like file names with common extensions, as these are unlikely to be intended as network domains and may be false positives from the regex.
579+
if (!fromUrl) {
580+
const lastLabel = host.slice(host.lastIndexOf('.') + 1);
581+
if (TerminalSandboxService._fileExtensionSuffixes.has(lastLabel)) {
582+
return undefined;
583+
}
584+
}
585+
571586
return hasWildcardPrefix ? `*.${host}` : host;
572587
}
573588

574589
private _matchesDomainPattern(domain: string, pattern: string): boolean {
575-
const normalizedPattern = this._normalizeDomain(this._extractDomainPattern(pattern));
590+
const normalizedPattern = this._normalizeDomain(this._extractDomainPattern(pattern), pattern.includes('://'));
576591
if (!normalizedPattern) {
577592
return false;
578593
}

src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,59 @@ suite('TerminalSandboxService - network domains', () => {
414414
strictEqual(wrapResult.blockedDomains, undefined, 'Malformed URL authorities should be ignored');
415415
});
416416

417+
test('should ignore file-extension suffixes that look like domains', async () => {
418+
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
419+
await sandboxService.getSandboxConfigPath();
420+
421+
const javascriptResult = sandboxService.wrapCommand('cat bundle.js', false, 'bash');
422+
strictEqual(javascriptResult.isSandboxWrapped, true, 'File extensions such as .js should not trigger blocked-domain prompts');
423+
strictEqual(javascriptResult.blockedDomains, undefined, 'File extensions such as .js should not be reported as domains');
424+
425+
const jsonResult = sandboxService.wrapCommand('cat package.json', false, 'bash');
426+
strictEqual(jsonResult.isSandboxWrapped, true, 'File extensions such as .json should not trigger blocked-domain prompts');
427+
strictEqual(jsonResult.blockedDomains, undefined, 'File extensions such as .json should not be reported as domains');
428+
});
429+
430+
test('should still treat URL authorities with file-like suffixes as domains', async () => {
431+
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
432+
await sandboxService.getSandboxConfigPath();
433+
434+
const wrapResult = sandboxService.wrapCommand('curl https://example.zip/path', false, 'bash');
435+
436+
strictEqual(wrapResult.isSandboxWrapped, false, 'URL authorities should still trigger blocked-domain prompts even when their suffix looks like a file extension');
437+
deepStrictEqual(wrapResult.blockedDomains, ['example.zip']);
438+
});
439+
440+
test('should still treat ssh remotes with file-like suffixes as domains', async () => {
441+
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
442+
await sandboxService.getSandboxConfigPath();
443+
444+
const wrapResult = sandboxService.wrapCommand('git clone git@example.zip:owner/repo.git', false, 'bash');
445+
446+
strictEqual(wrapResult.isSandboxWrapped, false, 'SSH remotes should still trigger blocked-domain prompts even when their suffix looks like a file extension');
447+
deepStrictEqual(wrapResult.blockedDomains, ['example.zip']);
448+
});
449+
450+
test('should not treat filenames in commands as domains', async () => {
451+
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
452+
await sandboxService.getSandboxConfigPath();
453+
454+
const commands = [
455+
'node server.js',
456+
'php index.php',
457+
'java -jar app.java',
458+
'cat styles.css',
459+
'cat README.md',
460+
'cat .env',
461+
];
462+
463+
for (const command of commands) {
464+
const wrapResult = sandboxService.wrapCommand(command, false, 'bash');
465+
strictEqual(wrapResult.isSandboxWrapped, true, `Command ${command} should remain sandboxed`);
466+
strictEqual(wrapResult.blockedDomains, undefined, `Command ${command} should not report a blocked domain`);
467+
}
468+
});
469+
417470
test('should not fall back to deprecated settings outside user scope', async () => {
418471
const originalInspect = configurationService.inspect.bind(configurationService);
419472
configurationService.inspect = <T>(key: string) => {

0 commit comments

Comments
 (0)