site-panzai/node_modules/launch-editor/construct-windows-launch-command.js

62 lines
2.8 KiB
JavaScript
Raw Normal View History

2026-11-13 13:57:04 +01:00
// 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