62 lines
2.8 KiB
JavaScript
62 lines
2.8 KiB
JavaScript
|
|
// On Windows, we need to use `exec` with the `shell: true` option,
|
||
|
|
// and some more sanitization is required.
|
||
|
|
|
||
|
|
// However, CMD.exe on Windows is vulnerable to RCE attacks given a file name of the
|
||
|
|
// form "C:\Users\myusername\Downloads\& curl 172.21.93.52".
|
||
|
|
// `create-react-app` used a safe file name pattern to validate user-provided file names:
|
||
|
|
// - https://github.com/facebook/create-react-app/pull/4866
|
||
|
|
// - https://github.com/facebook/create-react-app/pull/5431
|
||
|
|
// But that's not a viable solution for this package because
|
||
|
|
// it's depended on by so many meta frameworks that heavily rely on
|
||
|
|
// special characters in file names for filesystem-based routing.
|
||
|
|
// We need to at least:
|
||
|
|
// - Support `+` because it's used in SvelteKit and Vike
|
||
|
|
// - Support `$` because it's used in Remix
|
||
|
|
// - Support `(` and `)` because they are used in Analog, SolidStart, and Vike
|
||
|
|
// - Support `@` because it's used in Vike
|
||
|
|
// - Support `[` and `]` because they are widely used for [slug]
|
||
|
|
// So here we choose to use `^` to escape special characters instead.
|
||
|
|
|
||
|
|
// According to https://ss64.com/nt/syntax-esc.html,
|
||
|
|
// we can use `^` to escape `&`, `<`, `>`, `|`, `%`, and `^`
|
||
|
|
//
|
||
|
|
// `"` cannot appear in Windows paths, so it is outside the untrusted path input
|
||
|
|
// this sanitization protects.
|
||
|
|
function escapeCmdArgs(cmdArgs) {
|
||
|
|
return cmdArgs.replace(/([&|<>,;=^])/g, '^$1')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Need to double quote the editor path in case it contains spaces;
|
||
|
|
// If the fileName contains spaces, we also need to double quote it in the arguments
|
||
|
|
// However, there's a case that it's concatenated with line number and column number
|
||
|
|
// which is separated by `:`. We need to double quote the whole string in this case.
|
||
|
|
// Also, if the string contains the escape character `^`, it needs to be quoted, too.
|
||
|
|
function doubleQuoteIfNeeded(str) {
|
||
|
|
if (str.includes('^')) {
|
||
|
|
// If a string includes an escaped character, not only does it need to be quoted,
|
||
|
|
// but the quotes need to be escaped too.
|
||
|
|
return `^"${str}^"`
|
||
|
|
} else if (str.includes(' ')) {
|
||
|
|
return `"${str}"`
|
||
|
|
}
|
||
|
|
return str
|
||
|
|
}
|
||
|
|
|
||
|
|
function constructWindowsLaunchCommand(editor, args) {
|
||
|
|
// CMD treats line breaks as command separators. They cannot appear in regular Windows
|
||
|
|
// file names, but NTFS alternate data stream names may contain control characters.
|
||
|
|
if (args.some((arg) => /[\r\n]/.test(arg))) {
|
||
|
|
throw new Error('Cannot launch an editor with an argument containing a line break')
|
||
|
|
}
|
||
|
|
|
||
|
|
// CMD expands expressions such as `%VAR:old=new%` before consuming caret escapes, so
|
||
|
|
// percent signs in arguments cannot be preserved safely and are rejected.
|
||
|
|
if (args.some((arg) => arg.includes('%'))) {
|
||
|
|
throw new Error('Cannot launch an editor with an argument containing "%"')
|
||
|
|
}
|
||
|
|
|
||
|
|
return [editor, ...args.map(escapeCmdArgs)].map(doubleQuoteIfNeeded).join(' ')
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = constructWindowsLaunchCommand
|