mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Escape arguments when installer invokes commands
The usage of shell: true means that no escaping is done on the arguments
passed to the commands invoked by the installer. This leads to the
installation failing if the user for example puts a space in the
username for the admin account.
Fix by escaping the arguments passed on command lines. This is
unreasonably complicated on Windows 😡.
This commit is contained in:
1 parent
660af756e6
commit
70cf680edd
6 files changed
+161
-7
No files matched your search
@@ -16,6 +16,7 @@ const { copyPluginTemplates } = require("./template");
|
||||
let dev = false;
|
||||
const scriptExt = process.platform === "win32" ? ".cmd" : "";
|
||||
const finished = util.promisify(stream.finished);
|
||||
const { escapeArg } = require("./escape_arg");
|
||||
|
||||
// I hate this, there is a bug with rl.close on windows that was meant to have been fixed in Node14.4
|
||||
// See nodejs/node#21771 and nodejs/node#30701 for the apparent fix that was applied
|
||||
@@ -106,15 +107,25 @@ async function safeOutputFile(file, data, options={}) {
|
||||
}
|
||||
|
||||
async function execFile(cmd, args) {
|
||||
logger.verbose(`executing ${cmd} ${args.join(" ")}`);
|
||||
const escaped = args.map(escapeArg);
|
||||
logger.verbose(`executing ${cmd} ${escaped.join(" ")}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
let child = child_process.execFile(cmd, args, { shell: true }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
let child = child_process.execFile(
|
||||
cmd,
|
||||
escaped,
|
||||
{
|
||||
shell: true,
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
env: process.platform === "win32" ? { ...process.env, pct: "%" } : undefined,
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
let stdout = new LineSplitter({ readableObjectMode: true });
|
||||
stdout.on("data", line => { logger.verbose(line.toString()); });
|
||||
child.stdout.pipe(stdout);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
function escapeArg(arg) {
|
||||
// When shell: true is used execFile does not escaping of arguments, they are
|
||||
// simply joined by space and then passed to the shell.
|
||||
|
||||
// For Windows the command is cmd.exe /d /s /c "command arg arg ..."
|
||||
// The parsing and escaping on windows is cursed. The following references are not for mortals:
|
||||
// https://daviddeley.com/autohotkey/parameters/parameters.htm#WIN
|
||||
// https://stackoverflow.com/a/4095133
|
||||
// https://ss64.com/nt/syntax-esc.html
|
||||
if (process.platform === "win32") {
|
||||
if (!/[\t "%&<>\\^|]/.test(arg)) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
const escaped = [];
|
||||
let pos = 0;
|
||||
while (pos < arg.length) {
|
||||
if (arg[pos] === "\\") {
|
||||
// If a backslash is followed by a quote or the end of the string, then
|
||||
// double the number of backslashes because why not!
|
||||
let peek = pos + 1;
|
||||
while (peek !== arg.length && arg[peek] === "\\") {
|
||||
peek += 1;
|
||||
}
|
||||
if (peek === arg.length || arg[peek] === '"') {
|
||||
const count = peek - pos;
|
||||
escaped.push("\\".repeat(count * 2));
|
||||
pos = peek;
|
||||
continue;
|
||||
} else {
|
||||
escaped.push("\\");
|
||||
}
|
||||
} else if (arg[pos] === '"') {
|
||||
escaped.push('""');
|
||||
} else if (arg[pos] === "%") {
|
||||
// Expect a pct environment variable to contain %, because cmd does variable substitutions
|
||||
// before parsing string quotes, and there's no mechanism for escaping % symbols, and this
|
||||
// is a half sane workaround. The less sane workaround is to replace % with %%cd:~,%.
|
||||
escaped.push("%pct%");
|
||||
} else {
|
||||
escaped.push(arg[pos]);
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return `"${escaped.join("")}"`;
|
||||
}
|
||||
|
||||
// For all other platforms the command is /bin/sh -c <command arg arg ...>
|
||||
// The command and its arguments are passed as a single argument to the shell.
|
||||
// See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (!/[|&;<>()$`\\"' \t*?\[#~=%]/.test(arg)) {
|
||||
return arg;
|
||||
}
|
||||
return `"${arg.replace(/[$`"\\]/g, "\\$&")}"`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
escapeArg,
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
"use strict";
|
||||
if (module === require.main) {
|
||||
process.stdout.write(JSON.stringify(process.argv.slice(2)));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
const child_process = require("child_process");
|
||||
const util = require("util");
|
||||
const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const { escapeArg } = require("../../packages/create/escape_arg");
|
||||
const execFile = util.promisify(child_process.execFile);
|
||||
|
||||
async function exec(file, args) {
|
||||
const { stdout, stderr } = await execFile(
|
||||
file,
|
||||
args.map(escapeArg),
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
{ shell: true, cwd: __dirname, env: { ...process.env, pct: "%"} }
|
||||
);
|
||||
return { stdout: JSON.parse(stdout), stderr };
|
||||
}
|
||||
|
||||
const strings = [
|
||||
'"a"',
|
||||
"a sentence of text.",
|
||||
" irregular \tspaces ",
|
||||
'"C:\\path\\"',
|
||||
"C:\\path\\",
|
||||
'\\ "\\" \\\\ "\\\\" \\\\\\ "\\\\\\" \\\\\\\\ "\\\\\\\\" \\\\\\\\\\ "\\\\\\\\\\" \\\\\\\\\\\ "\\\\\\\\\\\\"',
|
||||
'"C:\\path"',
|
||||
"C:\\path",
|
||||
"%OS%",
|
||||
"!OS!",
|
||||
"😮",
|
||||
];
|
||||
for (let i=1; i < 128; i++) {
|
||||
const char = String.fromCodePoint(i);
|
||||
// Skip newline and carriage return because those are difficult to escape
|
||||
// Skip most alphanumeric characters as they don't behave differently to each other.
|
||||
if (/[\n\r1-9B-Zb-z]/.test(char)) {
|
||||
continue;
|
||||
}
|
||||
strings.push(char, `${char}${char}`, `${char}"`);
|
||||
}
|
||||
|
||||
// Test 100 strings at a time.
|
||||
const tests = [[]];
|
||||
for (const string of strings) {
|
||||
if (tests[tests.length - 1].length >= 100) {
|
||||
tests.push([]);
|
||||
}
|
||||
tests[tests.length - 1].push(string);
|
||||
}
|
||||
|
||||
|
||||
describe("create/escape_arg", function() {
|
||||
it("should correctly escape strings when args are forwarded", async function() {
|
||||
const ext = process.platform === "win32" ? ".cmd" : ".sh";
|
||||
for (const args of tests) {
|
||||
assert.deepEqual(
|
||||
await exec(`.${path.sep}forward_args${ext}`, ["echo_args.js", ...args]),
|
||||
{ stdout: args, stderr: "" }
|
||||
);
|
||||
}
|
||||
});
|
||||
it("should correctly escape strings when invoking node directly", async function() {
|
||||
for (const args of tests) {
|
||||
assert.deepEqual(
|
||||
await exec("node", ["echo_args.js", ...args]),
|
||||
{ stdout: args, stderr: "" }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
@ECHO off
|
||||
REM this is how npm and package.json bin entries forwards arguments on Windows
|
||||
node.exe %*
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# this is how package.json bin entries forwards arguments on Linux
|
||||
exec node "$@"
|
||||
Reference in new issue
Block a user