Skip to content

Fix hook commands that pass file URIs to spawn#4666

Open
darrenoh wants to merge 1 commit intomicrosoft:mainfrom
darrenoh:fix/304555-hook-file-uri-sanitization
Open

Fix hook commands that pass file URIs to spawn#4666
darrenoh wants to merge 1 commit intomicrosoft:mainfrom
darrenoh:fix/304555-hook-file-uri-sanitization

Conversation

@darrenoh
Copy link

Fixes microsoft/vscode#304555

Summary

  • sanitize file:// tokens in hook command strings before invoking spawn
  • preserve existing quoting and re-quote decoded paths that contain spaces
  • add regression tests for file URIs, quoted file URIs, spaced paths, and non-file URI passthrough

Test plan

  • Run NodeHookExecutor unit tests
  • Run npm run typecheck

@darrenoh
Copy link
Author

@microsoft-github-policy-service agree

@darrenoh darrenoh force-pushed the fix/304555-hook-file-uri-sanitization branch from feb9806 to 4800403 Compare March 24, 2026 21:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes hook command execution when commands include file:// URIs by rewriting those URI tokens into filesystem paths before running the command via spawn, and adds unit tests to prevent regressions in URI handling.

Changes:

  • Sanitize file://... tokens inside ChatHookCommand.command before invoking child_process.spawn.
  • Decode file URIs to filesystem paths and (re-)quote paths when needed (e.g., spaces).
  • Add unit tests covering file URI sanitization, quoting preservation, and non-file URI passthrough.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/platform/chat/node/hookExecutor.ts Sanitizes hook command strings by detecting file:// URI tokens, converting them to fsPath, and quoting when needed before spawn.
src/platform/chat/test/node/hookExecutor.spec.ts Adds regression tests asserting the sanitized command string passed to spawn for file URIs and non-file URIs.

return arg;
}

return `"${arg.replace(/["\\]/g, '\\$&')}"`;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quoteArgForShell wraps args in double quotes but does not escape $ or backticks. In /bin/sh -c ... (used when shell: true), $... and `...` are still expanded/executed inside double quotes, so a decoded file path containing these characters could change command behavior or allow command substitution. Consider switching to a POSIX-safe quoting strategy (e.g., single-quote with proper escaping) or explicitly escaping $ and backticks for the shell you invoke.

Suggested change
return `"${arg.replace(/["\\]/g, '\\$&')}"`;
if (!isWindows) {
// POSIX-safe single-quote escaping: 'foo' -> 'foo', "foo'bar" -> 'foo'"'"'bar'
let result = "'";
for (const ch of arg) {
if (ch === "'") {
result += `'"'"'`;
} else {
result += ch;
}
}
result += "'";
return result;
}
// PowerShell-safe single-quote escaping: 'foo' -> 'foo', "foo'bar" -> 'foo''bar'
return `'${arg.replace(/'/g, "''")}'`;

Copilot uses AI. Check for mistakes.
Comment on lines 50 to 55
private _spawn(hook: ChatHookCommand, input: unknown, token: CancellationToken): Promise<IHookCommandResult> {
const cwd = hook.cwd ? uriToFsPath(hook.cwd) : homedir();
const command = sanitizeHookCommand(hook.command);

const child = spawn(hook.command, [], {
const child = spawn(command, [], {
stdio: 'pipe',
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeCommand logs/errors refer to hookCommand.command, but _spawn now executes a sanitized command string. This can make diagnostics misleading (e.g., failures or timeouts will print the original file:// URI even though the executed command had a filesystem path/quoting). Consider logging/including the sanitized command (or both original + sanitized) anywhere the executed command is surfaced.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +194
test('quotes sanitized file URIs with spaces in hook commands', async () => {
const promise = executor.executeCommand(
cmd('cat file:///tmp/my%20file.txt'),
undefined,
CancellationToken.None
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new URI-sanitization tests cover spaces and quoting, but don't cover decoded shell metacharacters (e.g., %24 -> $ or %60 -> backtick). Since the sanitized string is executed via spawn(..., { shell: ... }), add a regression test that asserts these characters are escaped/quoted so they cannot trigger shell expansion/command substitution.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +193
}

const sanitizedPath = quote ? fsPath : quoteArgForShell(fsPath);
return `${prefix}${quote}${sanitizedPath}${quote}`;
});
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the original URI token is already quoted (quote is non-empty), sanitizeHookCommand injects the decoded fsPath without escaping for that quote style. On POSIX, paths can contain " or ', which would break the surrounding quotes (and could change shell parsing). Consider escaping the decoded path for the specific quote character, or always re-quoting with a known-safe quoting routine.

Copilot uses AI. Check for mistakes.
@darrenoh
Copy link
Author

@copilot open a new pull request to apply changes based on the comments in this thread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copilot Chat: child_process ENOPRO when file:// URIs are passed to commands

3 participants