Skip to content

Commit 3c2f9bf

Browse files
authored
fix: reject paths containing % on windows (#155)
fixes GHSA-698v-4r3x-3vcv
1 parent ecdfce1 commit 3c2f9bf

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

‎packages/launch-editor/construct-windows-launch-command.js‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
// According to https://ss64.com/nt/syntax-esc.html,
2121
// we can use `^` to escape `&`, `<`, `>`, `|`, `%`, and `^`
22-
// I'm not sure if we have to escape all of these, but let's do it anyway
22+
//
23+
// `"` cannot appear in Windows paths, so it is outside the untrusted path input
24+
// this sanitization protects.
2325
function escapeCmdArgs(cmdArgs) {
2426
return cmdArgs.replace(/([&|<>,;=^])/g, '^$1')
2527
}
@@ -41,6 +43,18 @@ function doubleQuoteIfNeeded(str) {
4143
}
4244

4345
function constructWindowsLaunchCommand(editor, args) {
46+
// CMD treats line breaks as command separators. They cannot appear in regular Windows
47+
// file names, but NTFS alternate data stream names may contain control characters.
48+
if (args.some((arg) => /[\r\n]/.test(arg))) {
49+
throw new Error('Cannot launch an editor with an argument containing a line break')
50+
}
51+
52+
// CMD expands expressions such as `%VAR:old=new%` before consuming caret escapes, so
53+
// percent signs in arguments cannot be preserved safely and are rejected.
54+
if (args.some((arg) => arg.includes('%'))) {
55+
throw new Error('Cannot launch an editor with an argument containing "%"')
56+
}
57+
4458
return [editor, ...args.map(escapeCmdArgs)].map(doubleQuoteIfNeeded).join(' ')
4559
}
4660

‎packages/launch-editor/construct-windows-launch-command.test.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,32 @@ describe('constructWindowsLaunchCommand', () => {
4040
)
4141
})
4242

43+
for (const [name, lineBreak] of [
44+
['carriage returns', '\r'],
45+
['line feeds', '\n'],
46+
]) {
47+
test(`rejects ${name} in arguments`, () => {
48+
assert.throws(
49+
() => constructWindowsLaunchCommand('code', [`C:\\project\\file.js${lineBreak}calc.exe`]),
50+
/argument containing a line break/,
51+
)
52+
})
53+
}
54+
55+
test('rejects percent signs in arguments', () => {
56+
assert.throws(
57+
() => constructWindowsLaunchCommand('code', ['C:\\project\\100%\\file.js']),
58+
/argument containing "%"/,
59+
)
60+
})
61+
62+
test('rejects CMD variable modifiers', () => {
63+
assert.throws(
64+
() => constructWindowsLaunchCommand('code', ['C:\\project\\%VAR:old=new%']),
65+
/argument containing "%"/,
66+
)
67+
})
68+
4369
test('quotes arguments containing both spaces and escaped CMD metacharacters', () => {
4470
assert.equal(
4571
constructWindowsLaunchCommand('code', ['C:\\my project\\&file.js']),

‎packages/launch-editor/index.js‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ function launchEditor(file, specifiedEditor, onErrorCallback) {
9393
const extraArgs = getArgumentsForFile(editor, fileName, lineNumber, columnNumber)
9494
args.push.apply(args, extraArgs)
9595

96+
let launchCommand
97+
if (process.platform === 'win32') {
98+
try {
99+
launchCommand = constructWindowsLaunchCommand(editor, args)
100+
} catch (error) {
101+
onErrorCallback(fileName, error.message)
102+
return
103+
}
104+
}
105+
96106
if (currentChildProcess && isTerminalEditor(editor)) {
97107
// There's an existing editor process already and it's attached
98108
// to the terminal, so go kill it. Otherwise two separate editor
@@ -101,7 +111,6 @@ function launchEditor(file, specifiedEditor, onErrorCallback) {
101111
}
102112

103113
if (process.platform === 'win32') {
104-
const launchCommand = constructWindowsLaunchCommand(editor, args)
105114
currentChildProcess = childProcess.exec(launchCommand, {
106115
stdio: 'inherit',
107116
shell: true,

0 commit comments

Comments
 (0)