Skip to content

Dropped file paths are quoted for cmd.exe even in a PowerShell pane, allowing subexpression execution #430

Description

@nedtwigg

Found by the nightly security audit's application-security pass (see SECURITY.md → CI Validation Contract), verified by hand.

What happens

shellEscapePath picks its quoting rule from the host platform, never from the shell running in the target pane:

https://github.com/diffplug/dormouse/blob/main/lib/src/lib/shell-escape.ts#L27-L29

export function shellEscapePath(input: string): string {
  return !IS_MAC && IS_WINDOWS ? shellEscapeWindows(input) : shellEscapePosix(input);
}

shellEscapeWindows wraps the path in double quotes and doubles any embedded ". That is correct for cmd.exe, which does no expansion inside double quotes. It is not correct for PowerShell, where a double-quoted string is expandable: $(...) runs a subexpression and $name interpolates.

So on Windows, dropping (or pasting) a file named

$(calc.exe).txt

into a PowerShell pane stages the line

"$(calc.exe).txt"

and the subexpression runs as soon as the user presses Enter — which they have every reason to do, since they just dropped a file in to use its path. The single caller is the drop/paste path:

https://github.com/diffplug/dormouse/blob/main/lib/src/lib/clipboard.ts#L66

const text = paths.map(shellEscapePath).join(' ') + ' ';

Severity

Low-to-moderate. It needs a Windows host, a PowerShell pane, and a file whose name the attacker controls — an unzipped archive, a shared folder, a download. Nothing is executed without the user pressing Enter. But the whole point of drag-and-drop is that the resulting line looks like an ordinary path and gets run without inspection, and a filename is exactly the kind of thing users treat as inert data.

The fix already exists one package over

dor quotes per target shell rather than per host platform, and gets PowerShell right:

https://github.com/diffplug/dormouse/blob/main/dor/src/commands/shell-quote.ts#L13-L21

The work is threading the pane's shell identity to the call site in clipboard.ts and reusing that logic instead of shellEscapePath's platform branch. Worth checking whether anything else infers a shell from process.platform.

docs/specs/mouse-and-clipboard.md owns this behavior and should be updated with the fix — it currently describes the escaping without noting that it assumes the host's default shell.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions