From 29ca83a576271c46cb6dcaf65208178e8d3b29e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20C=C3=A1ceres?= Date: Tue, 14 Apr 2026 08:17:18 +1000 Subject: [PATCH 1/7] Potential fix for code scanning alert no. 47: Shell command built from environment values Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- tools/release.cjs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/release.cjs b/tools/release.cjs index f7e967cd15..74a0bc0fa1 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -3,7 +3,7 @@ const { Builder } = require("./builder.cjs"); const cmdPrompt = require("prompt"); const colors = require("colors"); -const { exec } = require("child_process"); +const { execFile } = require("child_process"); const loading = require("loading-indicator"); const DEBUG = false; const vnu = require("vnu-jar"); @@ -35,8 +35,15 @@ const loadOps = { delay: 100, }; +function splitArgs(input) { + return input.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(part => part.replace(/^"|"$/g, "")) ?? []; +} + /** @param {string} program */ function commandRunner(program) { + const programParts = splitArgs(program); + const file = programParts[0]; + const baseArgs = programParts.slice(1); /** * @param {string} cmd * @param {{showOutput: boolean}} [options ] @@ -46,7 +53,8 @@ function commandRunner(program) { if (DEBUG) { return Promise.resolve(""); } - return toExecPromise(`${program} ${cmd}`, { ...options, timeout: 200000 }); + const args = [...baseArgs, ...splitArgs(cmd)]; + return toExecFilePromise(file, args, { ...options, timeout: 200000 }); }; return runner; } @@ -253,17 +261,18 @@ const Prompts = { /** * - * @param {string} cmd + * @param {string} file + * @param {string[]} args * @param {{ timeout: number, showOutput: boolean }} options * @returns {Promise} */ -function toExecPromise(cmd, { timeout, showOutput }) { +function toExecFilePromise(file, args, { timeout, showOutput }) { return new Promise((resolve, reject) => { const id = setTimeout(() => { - reject(new Error(`Command took too long: ${cmd}`)); + reject(new Error(`Command took too long: ${file} ${args.join(" ")}`)); proc.kill("SIGTERM"); }, timeout); - const proc = exec(cmd, (err, stdout) => { + const proc = execFile(file, args, (err, stdout) => { clearTimeout(id); if (err) { return reject(err); From 93d014ccf8b36aa4a00da508a0d60b6c503b5319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:03:44 +0000 Subject: [PATCH 2/7] fix(tools): use execFile instead of exec to avoid shell injection Agent-Logs-Url: https://github.com/speced/respec/sessions/cf64d910-0bb6-4b8e-8a7b-e6ebd7300833 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- tools/release.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/release.cjs b/tools/release.cjs index 74a0bc0fa1..619eb74083 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -35,6 +35,7 @@ const loadOps = { delay: 100, }; +/** @param {string} input */ function splitArgs(input) { return input.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(part => part.replace(/^"|"$/g, "")) ?? []; } From 53eaa195b23de045a7d6c2ea91a2bd072d655606 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:05:14 +0000 Subject: [PATCH 3/7] fix(tools): format splitArgs with prettier Agent-Logs-Url: https://github.com/speced/respec/sessions/cf64d910-0bb6-4b8e-8a7b-e6ebd7300833 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- tools/release.cjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/release.cjs b/tools/release.cjs index 619eb74083..d358530c2f 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -37,7 +37,11 @@ const loadOps = { /** @param {string} input */ function splitArgs(input) { - return input.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(part => part.replace(/^"|"$/g, "")) ?? []; + return ( + input + .match(/(?:[^\s"]+|"[^"]*")+/g) + ?.map(part => part.replace(/^"|"$/g, "")) ?? [] + ); } /** @param {string} program */ From 3460e1774124829a6e1e57c076ebcae18a23c079 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Fri, 17 Apr 2026 17:53:57 +1000 Subject: [PATCH 4/7] fix(tools): use structured args instead of regex parser Replace the hand-rolled splitArgs regex parser with direct array construction. The regex failed on paths with spaces (e.g. vnu-jar path) and was unnecessary complexity for static command strings. --- tools/release.cjs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tools/release.cjs b/tools/release.cjs index d358530c2f..6652261841 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -35,30 +35,21 @@ const loadOps = { delay: 100, }; -/** @param {string} input */ -function splitArgs(input) { - return ( - input - .match(/(?:[^\s"]+|"[^"]*")+/g) - ?.map(part => part.replace(/^"|"$/g, "")) ?? [] - ); -} - -/** @param {string} program */ -function commandRunner(program) { - const programParts = splitArgs(program); - const file = programParts[0]; - const baseArgs = programParts.slice(1); +/** + * @param {string} file + * @param {string[]} [baseArgs] + */ +function commandRunner(file, baseArgs = []) { /** * @param {string} cmd - * @param {{showOutput: boolean}} [options ] + * @param {{showOutput: boolean}} [options] */ const runner = (cmd, options = { showOutput: false }) => { - console.log(colors.cyan(`Run: ${program} ${colors.grey(cmd)}`)); + const args = [...baseArgs, ...cmd.split(/\s+/).filter(Boolean)]; + console.log(colors.cyan(`Run: ${file} ${colors.grey(args.join(" "))}`)); if (DEBUG) { return Promise.resolve(""); } - const args = [...baseArgs, ...splitArgs(cmd)]; return toExecFilePromise(file, args, { ...options, timeout: 200000 }); }; return runner; @@ -67,7 +58,7 @@ function commandRunner(program) { const git = commandRunner("git"); const npm = commandRunner("npm"); const node = commandRunner("node"); -const validator = commandRunner(`java -jar ${vnu}`); +const validator = commandRunner("java", ["-jar", vnu]); cmdPrompt.start(); From 0a7224bce9646c373cf260d8d9f487852d1b8c87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:30:43 +0000 Subject: [PATCH 5/7] fix(tools): accept string[] cmd to preserve quoted args and paths with spaces Agent-Logs-Url: https://github.com/speced/respec/sessions/5d94d008-6cad-43d1-89d6-8907cf192c39 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- tools/release.cjs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tools/release.cjs b/tools/release.cjs index 6652261841..479d757370 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -41,11 +41,12 @@ const loadOps = { */ function commandRunner(file, baseArgs = []) { /** - * @param {string} cmd + * @param {string | string[]} cmd * @param {{showOutput: boolean}} [options] */ const runner = (cmd, options = { showOutput: false }) => { - const args = [...baseArgs, ...cmd.split(/\s+/).filter(Boolean)]; + const cmdArgs = Array.isArray(cmd) ? cmd : cmd.split(/\s+/).filter(Boolean); + const args = [...baseArgs, ...cmdArgs]; console.log(colors.cyan(`Run: ${file} ${colors.grey(args.join(" "))}`)); if (DEBUG) { return Promise.resolve(""); @@ -361,7 +362,13 @@ const run = async () => { // 2. Bump the version in `package.json`. const version = await Prompts.askBumpVersion(); await Prompts.askBuildAddCommitMergeTag(); - await npm(`version ${version} -m "v${version}" --no-git-tag-version`); + await npm([ + "version", + version, + "-m", + `v${version}`, + "--no-git-tag-version", + ]); // 3. Run the build script (node tools/builder.js). await npm("run builddeps"); @@ -371,19 +378,22 @@ const run = async () => { console.log(colors.green(" Making sure the generated version is ok... šŸ•µšŸ»")); const source = `file:///${__dirname}/../examples/basic.built.html`; const tempFile = path.join(os.tmpdir(), "index.html"); - await node(`./tools/respec2html.js -e --timeout 30 ${source} ${tempFile}`, { - showOutput: true, - }); + await node( + ["./tools/respec2html.js", "-e", "--timeout", "30", source, tempFile], + { + showOutput: true, + } + ); // Do HTML validation console.log(colors.green(" Making sure HTML validator is happy... šŸ•µšŸ»")); - await validator(`--stdout ${tempFile}`); + await validator(["--stdout", tempFile]); console.log(colors.green(" Build Seems good... āœ…")); // 4. Commit your changes await git("add builds package.json pnpm-lock.yaml"); - await git(`commit -m "v${version}"`); - await git(`tag "v${version}"`); + await git(["commit", "-m", `v${version}`]); + await git(["tag", `v${version}`]); // 5. Merge to gh-pages (git checkout gh-pages; git merge main) await git("checkout gh-pages"); From c4ed9bdb1fcea3a4cafaa2ac31828bf4c33920b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:33:06 +0000 Subject: [PATCH 6/7] fix(tools): convert all command runner calls to array syntax Agent-Logs-Url: https://github.com/speced/respec/sessions/5d94d008-6cad-43d1-89d6-8907cf192c39 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- tools/release.cjs | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tools/release.cjs b/tools/release.cjs index 479d757370..214284c302 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -87,7 +87,7 @@ const Prompts = { default: "y", }; await this.askQuestion(promptOps); - await git(`checkout ${to}`); + await git(["checkout", to]); }, /** @param {string} branch */ @@ -99,7 +99,7 @@ const Prompts = { default: "y", }; await this.askQuestion(promptOps); - await git(`pull origin ${branch}`); + await git(["pull", "origin", branch]); }, async askUpToDateAndDev() { @@ -207,10 +207,14 @@ const Prompts = { }, async askBumpVersion() { - const rawVersion = await npm("view respec version"); + const rawVersion = await npm(["view", "respec", "version"]); const version = rawVersion.trim(); - const latestTag = await git("describe --tags --abbrev=0"); - const commits = await git(`log ${latestTag.trim()}..HEAD --oneline`); + const latestTag = await git(["describe", "--tags", "--abbrev=0"]); + const commits = await git([ + "log", + `${latestTag.trim()}..HEAD`, + "--oneline", + ]); if (!commits) { throw new Error("😢 No commits. Nothing to release."); } @@ -290,9 +294,9 @@ function toExecFilePromise(file, args, { timeout, showOutput }) { } async function getBranchState() { - const local = await git("rev-parse @"); - const remote = await git("rev-parse @{u}"); - const base = await git("merge-base @ @{u}"); + const local = await git(["rev-parse", "@"]); + const remote = await git(["rev-parse", "@{u}"]); + const base = await git(["merge-base", "@", "@{u}"]); let result = ""; switch (local) { case remote: @@ -308,7 +312,7 @@ async function getBranchState() { } async function getCurrentBranch() { - const branch = await git("rev-parse --abbrev-ref HEAD"); + const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]); return branch.trim(); } @@ -340,7 +344,7 @@ const run = async () => { try { // 1. Confirm maintainer is on up-to-date and on the main branch () indicators.get("remote-update").show(); - await git("remote update"); + await git(["remote", "update"]); indicators.get("remote-update").hide(); if (initialBranch !== "main") { await Prompts.askSwitchToBranch(initialBranch, "main"); @@ -371,7 +375,7 @@ const run = async () => { ]); // 3. Run the build script (node tools/builder.js). - await npm("run builddeps"); + await npm(["run", "builddeps"]); for (const name of ["w3c", "geonovum", "dini", "aom"]) { await Builder.build({ name }); } @@ -391,23 +395,23 @@ const run = async () => { console.log(colors.green(" Build Seems good... āœ…")); // 4. Commit your changes - await git("add builds package.json pnpm-lock.yaml"); + await git(["add", "builds", "package.json", "pnpm-lock.yaml"]); await git(["commit", "-m", `v${version}`]); await git(["tag", `v${version}`]); // 5. Merge to gh-pages (git checkout gh-pages; git merge main) - await git("checkout gh-pages"); - await git("pull origin gh-pages"); - await git("merge main"); - await git("checkout main"); + await git(["checkout", "gh-pages"]); + await git(["pull", "origin", "gh-pages"]); + await git(["merge", "main"]); + await git(["checkout", "main"]); await Prompts.askPushAll(); indicators.get("push-to-server").show(); - await git("push origin main"); - await git("push origin gh-pages"); - await git("push --tags"); + await git(["push", "origin", "main"]); + await git(["push", "origin", "gh-pages"]); + await git(["push", "--tags"]); indicators.get("push-to-server").hide(); console.log(colors.green(" Publishing to npm... šŸ“”")); - await npm("publish", { showOutput: true }); + await npm(["publish"], { showOutput: true }); if (initialBranch !== "main") { await Prompts.askSwitchToBranch("main", initialBranch); } @@ -415,7 +419,7 @@ const run = async () => { console.error(colors.red(`\n☠ ${err.stack}`)); const currentBranch = await getCurrentBranch(); if (initialBranch !== currentBranch) { - await git(`checkout ${initialBranch}`); + await git(["checkout", initialBranch]); } process.exit(1); return; From 1704c95f4d876b1c60af13238bb190514d5b0f2a Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 18 Apr 2026 05:21:50 +1000 Subject: [PATCH 7/7] chore(tools): remove dead string fallback in commandRunner All call sites now pass arrays. The string split path was dead code that could silently break on spaced arguments. --- tools/release.cjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/release.cjs b/tools/release.cjs index 214284c302..a5668d8c3a 100755 --- a/tools/release.cjs +++ b/tools/release.cjs @@ -41,12 +41,11 @@ const loadOps = { */ function commandRunner(file, baseArgs = []) { /** - * @param {string | string[]} cmd + * @param {string[]} cmd * @param {{showOutput: boolean}} [options] */ const runner = (cmd, options = { showOutput: false }) => { - const cmdArgs = Array.isArray(cmd) ? cmd : cmd.split(/\s+/).filter(Boolean); - const args = [...baseArgs, ...cmdArgs]; + const args = [...baseArgs, ...cmd]; console.log(colors.cyan(`Run: ${file} ${colors.grey(args.join(" "))}`)); if (DEBUG) { return Promise.resolve("");