Skip to content

Commit f6e6701

Browse files
authored
fix: ensure filenames passed to editors are not considered as flags (#156)
fixes GHSA-r825-w8mm-5pxw
1 parent 3c2f9bf commit f6e6701

2 files changed

Lines changed: 109 additions & 27 deletions

File tree

‎packages/launch-editor/get-args.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const path = require('path')
33

44
// normalize a file and optional line/column numbers into command line args for specific editors
55
module.exports = function getArgumentsForFile(editor, fileName, lineNumber, columnNumber = 1) {
6+
fileName = path.resolve(fileName)
7+
68
if (
79
process.platform === 'linux' &&
810
fileName.startsWith('/mnt/') &&
@@ -14,7 +16,7 @@ module.exports = function getArgumentsForFile(editor, fileName, lineNumber, colu
1416
// build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
1517
// When a Windows editor is specified, interop functionality can
1618
// handle the path translation, but only if a relative path is used.
17-
fileName = path.relative('', fileName)
19+
fileName = `./${path.relative('', fileName)}`
1820
}
1921

2022
if (!lineNumber) {
Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
const assert = require('node:assert/strict')
22
const os = require('node:os')
3+
const path = require('node:path')
34
const { describe, test } = require('node:test')
45

56
const getArgumentsForFile = require('./get-args.js')
7+
const absoluteFileName = path.resolve('file')
68

79
describe('getArgumentsForFile', () => {
8-
test('uses the file name as the only argument when no position is given', () => {
9-
assert.deepEqual(getArgumentsForFile('vim', 'file'), ['file'])
10+
test('uses an absolute file name as the only argument when no position is given', () => {
11+
assert.deepEqual(getArgumentsForFile('vim', absoluteFileName), [absoluteFileName])
1012
})
1113

12-
test('uses a relative path for files on a Windows drive in WSL', (t) => {
14+
test('resolves a relative file name to an absolute path', () => {
15+
assert.deepEqual(getArgumentsForFile('vim', 'file'), [absoluteFileName])
16+
})
17+
18+
describe('WSL handling', () => {
1319
if (process.platform !== 'linux') {
14-
t.skip('not running on Linux')
1520
return
1621
}
1722

18-
t.mock.method(process, 'cwd', () => '/home/user/project')
19-
t.mock.method(os, 'release', () => '4.4.0-43-Microsoft')
23+
test('uses a relative path for files on a Windows drive in WSL', (t) => {
24+
t.mock.method(process, 'cwd', () => '/home/user/project')
25+
t.mock.method(os, 'release', () => '4.4.0-43-Microsoft')
26+
27+
const fileName = '/mnt/c/project/file.js'
28+
assert.deepEqual(getArgumentsForFile('vim', fileName), ['./../../../mnt/c/project/file.js'])
29+
})
30+
31+
test('resolves a relative path before converting it for WSL', (t) => {
32+
t.mock.method(process, 'cwd', () => '/mnt/c/project')
33+
t.mock.method(os, 'release', () => '4.4.0-43-Microsoft')
34+
35+
assert.deepEqual(getArgumentsForFile('vim', 'file.js'), ['./file.js'])
36+
})
2037

21-
const fileName = '/mnt/c/project/file.js'
22-
assert.deepEqual(getArgumentsForFile('vim', fileName), ['../../../mnt/c/project/file.js'])
38+
test('prefixes an option-like relative path on a Windows drive in WSL', (t) => {
39+
t.mock.method(process, 'cwd', () => '/mnt/c/project')
40+
t.mock.method(os, 'release', () => '4.4.0-43-Microsoft')
41+
42+
const fileName = '+call system("touch LAUNCH_EDITOR_MARKER")'
43+
assert.deepEqual(getArgumentsForFile('vim', fileName), [
44+
'./+call system("touch LAUNCH_EDITOR_MARKER")',
45+
])
46+
})
2347
})
2448

2549
describe('editor-specific argument formats', () => {
@@ -35,38 +59,54 @@ describe('getArgumentsForFile', () => {
3559
'zed',
3660
]) {
3761
test(`${editor} uses "file:line:column"`, () => {
38-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), ['file:10:5'])
62+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
63+
`${absoluteFileName}:10:5`,
64+
])
3965
})
4066
}
4167

4268
test('notepad++ uses -n/-c flags', () => {
43-
assert.deepEqual(getArgumentsForFile('notepad++', 'file', 10, 5), ['-n10', '-c5', 'file'])
69+
assert.deepEqual(getArgumentsForFile('notepad++', absoluteFileName, 10, 5), [
70+
'-n10',
71+
'-c5',
72+
absoluteFileName,
73+
])
4474
})
4575

4676
for (const editor of ['vim', 'mvim']) {
4777
test(`${editor} uses +call cursor()`, () => {
48-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), [
78+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
4979
'+call cursor(10, 5)',
50-
'file',
80+
absoluteFileName,
5181
])
5282
})
5383
}
5484

5585
for (const editor of ['joe', 'gvim']) {
5686
test(`${editor} uses +line`, () => {
57-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), ['+10', 'file'])
87+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
88+
'+10',
89+
absoluteFileName,
90+
])
5891
})
5992
}
6093

6194
for (const editor of ['emacs', 'emacsclient']) {
6295
test(`${editor} uses +line:column`, () => {
63-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), ['+10:5', 'file'])
96+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
97+
'+10:5',
98+
absoluteFileName,
99+
])
64100
})
65101
}
66102

67103
for (const editor of ['rmate', 'mate', 'mine']) {
68104
test(`${editor} uses --line`, () => {
69-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), ['--line', 10, 'file'])
105+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
106+
'--line',
107+
10,
108+
absoluteFileName,
109+
])
70110
})
71111
}
72112

@@ -83,56 +123,96 @@ describe('getArgumentsForFile', () => {
83123
'VSCodium',
84124
]) {
85125
test(`${editor} uses -r -g "file:line:column"`, () => {
86-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), ['-r', '-g', 'file:10:5'])
126+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
127+
'-r',
128+
'-g',
129+
`${absoluteFileName}:10:5`,
130+
])
87131
})
88132
}
89133

90134
for (const editor of ['idea', 'idea64', 'webstorm', 'pycharm', 'clion', 'rider']) {
91135
test(`${editor} uses --line/--column`, () => {
92-
assert.deepEqual(getArgumentsForFile(editor, 'file', 10, 5), [
136+
assert.deepEqual(getArgumentsForFile(editor, absoluteFileName, 10, 5), [
93137
'--line',
94138
10,
95139
'--column',
96140
5,
97-
'file',
141+
absoluteFileName,
98142
])
99143
})
100144
}
101145
})
102146

103147
test('defaults columnNumber to 1 when omitted', () => {
104-
assert.deepEqual(getArgumentsForFile('code', 'file', 10), ['-r', '-g', 'file:10:1'])
148+
assert.deepEqual(getArgumentsForFile('code', absoluteFileName, 10), [
149+
'-r',
150+
'-g',
151+
`${absoluteFileName}:10:1`,
152+
])
105153
})
106154

107155
describe('editor resolution via path.basename and extension stripping', () => {
108156
test('resolves a full POSIX path', () => {
109-
assert.deepEqual(getArgumentsForFile('/usr/local/bin/code', 'file', 10, 5), [
157+
assert.deepEqual(getArgumentsForFile('/usr/local/bin/code', absoluteFileName, 10, 5), [
110158
'-r',
111159
'-g',
112-
'file:10:5',
160+
`${absoluteFileName}:10:5`,
113161
])
114162
})
115163

116164
if (process.platform === 'win32') {
117165
test('resolves a full Windows path with .exe', () => {
118-
assert.deepEqual(getArgumentsForFile('C:\\path\\Code.exe', 'file', 10, 5), [
166+
assert.deepEqual(getArgumentsForFile('C:\\path\\Code.exe', absoluteFileName, 10, 5), [
119167
'-r',
120168
'-g',
121-
'file:10:5',
169+
`${absoluteFileName}:10:5`,
122170
])
123171
})
124172

125173
test('resolves notepad++ from a full path with .exe', () => {
126-
assert.deepEqual(getArgumentsForFile('C:\\tools\\notepad++.exe', 'file', 10, 5), [
174+
assert.deepEqual(getArgumentsForFile('C:\\tools\\notepad++.exe', absoluteFileName, 10, 5), [
127175
'-n10',
128176
'-c5',
129-
'file',
177+
absoluteFileName,
130178
])
131179
})
132180
}
133181

134182
test('strips .cmd extension case-insensitively', () => {
135-
assert.deepEqual(getArgumentsForFile('code.CMD', 'file', 10, 5), ['-r', '-g', 'file:10:5'])
183+
assert.deepEqual(getArgumentsForFile('code.CMD', absoluteFileName, 10, 5), [
184+
'-r',
185+
'-g',
186+
`${absoluteFileName}:10:5`,
187+
])
136188
})
137189
})
190+
191+
describe('file names that look like editor options', () => {
192+
const cases = [
193+
['vim', '+call writefile(["marker"], "LAUNCH_EDITOR_MARKER")|qa!'],
194+
['mvim', '-ccall writefile(["marker"], "LAUNCH_EDITOR_MARKER")|qa!'],
195+
['gvim', '+call writefile(["marker"], "LAUNCH_EDITOR_MARKER")|qa!'],
196+
['emacs', '--eval=(write-region "marker" nil "LAUNCH_EDITOR_MARKER")'],
197+
['emacsclient', '-eatouch LAUNCH_EDITOR_MARKER'],
198+
['notepad++', '-pluginMessage=LAUNCH_EDITOR_MARKER'],
199+
]
200+
201+
for (const [editor, fileName] of cases) {
202+
for (const lineNumber of [undefined, 10]) {
203+
const position = lineNumber ? 'with a position' : 'without a position'
204+
205+
test(`${editor} protects an option-like file name ${position}`, () => {
206+
const args = getArgumentsForFile(editor, fileName, lineNumber, 5)
207+
const fileArgument = args.at(-1)
208+
209+
assert.equal(path.resolve(fileArgument), path.resolve(fileName))
210+
assert.ok(
211+
path.isAbsolute(fileArgument),
212+
`unsafe editor arguments: ${JSON.stringify(args)}`,
213+
)
214+
})
215+
}
216+
}
217+
})
138218
})

0 commit comments

Comments
 (0)