From ddc8aa588cd24aec862cfc2880d431dff18c2880 Mon Sep 17 00:00:00 2001 From: Ryan Brodkin <236564+brodkin@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:51:34 -0700 Subject: [PATCH] Escape interpolated values in --dry-run curl preview The --dry-run preview built its curl command by concatenating the URL and JSON body inside single quotes without escaping. A value containing a single quote, such as an Egnyte path with an apostrophe, terminated the quoted string and produced a syntactically broken command. A crafted path could also inject additional shell commands into the previewed output, which a user is invited to copy and run. Add shellQuote() and use it for the URL, JSON body, and multipart file argument so interpolated values are always safely quoted. --- src/lib/output.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/lib/output.js b/src/lib/output.js index 08e661a..7d9909a 100644 --- a/src/lib/output.js +++ b/src/lib/output.js @@ -53,18 +53,29 @@ function info(msg) { process.stderr.write(msg + '\n'); } +/** + * Quote a value as a single-quoted POSIX shell word. An embedded single + * quote is closed, escaped, and reopened ('\'') so the value cannot end the + * quoting. Without this, a value that contains a quote (for example an + * Egnyte path with an apostrophe) breaks out of the quoted string in the + * previewed curl command, and a crafted path can inject arbitrary commands. + */ +function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\\''") + "'"; +} + function formatDryRun(opts) { const { method, url, body, bodyType, localFile } = opts; const parts = [ - "curl -X " + method + " '" + url + "'", + 'curl -X ' + method + ' ' + shellQuote(url), " -H 'Authorization: ***'", ]; if (bodyType === 'json') { parts.push(" -H 'Content-Type: application/json'"); - parts.push(" -d '" + JSON.stringify(body) + "'"); + parts.push(' -d ' + shellQuote(JSON.stringify(body))); } else if (bodyType === 'multipart') { parts.push(" -H 'Content-Type: multipart/form-data'"); - parts.push(" -F 'file=@" + localFile + "'"); + parts.push(' -F ' + shellQuote('file=@' + localFile)); } return parts.join(' \\\n'); } @@ -113,4 +124,4 @@ function serializeError(err) { return payload; } -module.exports = { CLIError, out, fatal, info, formatDryRun, printDryRun, requireConfirmation, serializeError }; +module.exports = { CLIError, out, fatal, info, shellQuote, formatDryRun, printDryRun, requireConfirmation, serializeError };