From 34fbe007f099c8a1c419d7b90f825f5d3a743810 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 23 Jul 2026 21:07:59 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20reject=20credential-bearing=20It=C5=8D?= =?UTF-8?q?=20CLI=20shims=20(#2559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ito.js | 50 +++++++++++------- tests/scripts/ito-cli-bridge.test.js | 76 +++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 18 deletions(-) diff --git a/scripts/ito.js b/scripts/ito.js index 0984b5c1b..85f541b03 100755 --- a/scripts/ito.js +++ b/scripts/ito.js @@ -10,6 +10,12 @@ const { createSafeItoEnvironment } = require("./lib/ito-environment"); const SUPPORTED_COMMANDS = Object.freeze(["auth", "find", "status"]); const CANONICAL_REPOSITORY = "https://github.com/Ito-Markets/ito-cloud-runtime.git"; const CANONICAL_PACKAGE_PATH = "cli/ito-compute-cli"; +const CANONICAL_ENTRY_SEGMENTS = Object.freeze([ + ...CANONICAL_PACKAGE_PATH.split("/"), + "dist", + "bin", + "ito.js", +]); const EXECUTABLE_OVERRIDE = "ECC_ITO_CLI_EXECUTABLE"; const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; @@ -127,6 +133,11 @@ function assertUsableExecutable(candidate) { `${EXECUTABLE_OVERRIDE} does not point to a readable local Itô CLI file.` ); } + if (!isCanonicalItoEntry(canonicalCandidate)) { + throw new Error( + `${EXECUTABLE_OVERRIDE} must point to the canonical dist/bin/ito.js entry.` + ); + } if (!isUsableExecutable(canonicalCandidate)) { throw new Error( `${EXECUTABLE_OVERRIDE} does not point to a readable local Itô CLI file.` @@ -135,15 +146,26 @@ function assertUsableExecutable(candidate) { return canonicalCandidate; } +function isCanonicalItoEntry(candidate) { + const pathSegments = path + .normalize(candidate) + .split(path.sep) + .filter(Boolean); + if (pathSegments.length < CANONICAL_ENTRY_SEGMENTS.length) return false; + const candidateTail = pathSegments.slice(-CANONICAL_ENTRY_SEGMENTS.length); + return candidateTail.every((segment, index) => { + const expected = CANONICAL_ENTRY_SEGMENTS[index]; + return process.platform === "win32" + ? segment.toLowerCase() === expected.toLowerCase() + : segment === expected; + }); +} + function isUsableExecutable(candidate) { try { const info = fs.statSync(candidate); if (!info.isFile()) return false; - if (process.platform !== "win32" && path.extname(candidate) !== ".js") { - fs.accessSync(candidate, fs.constants.X_OK); - } else { - fs.accessSync(candidate, fs.constants.R_OK); - } + fs.accessSync(candidate, fs.constants.R_OK); return true; } catch { return false; @@ -151,21 +173,15 @@ function isUsableExecutable(candidate) { } function buildInvocation(executable, args) { - if (path.extname(executable).toLowerCase() === ".js") { - return Object.freeze({ - executable: process.execPath, - args: Object.freeze([executable, ...args]), - }); - } - if ( - process.platform === "win32" - && /\.(?:bat|cmd|ps1)$/i.test(executable) - ) { + if (!isCanonicalItoEntry(executable)) { throw new Error( - `Refusing to invoke the Itô CLI through a shell shim. Set ${EXECUTABLE_OVERRIDE} to the absolute dist/bin/ito.js path.` + `Refusing to invoke an Itô CLI shim. Set ${EXECUTABLE_OVERRIDE} to the absolute dist/bin/ito.js path.` ); } - return Object.freeze({ executable, args: Object.freeze([...args]) }); + return Object.freeze({ + executable: process.execPath, + args: Object.freeze([executable, ...args]), + }); } function invokeIto(executable, args, environment = process.env) { diff --git a/tests/scripts/ito-cli-bridge.test.js b/tests/scripts/ito-cli-bridge.test.js index d170def00..ef896290e 100644 --- a/tests/scripts/ito-cli-bridge.test.js +++ b/tests/scripts/ito-cli-bridge.test.js @@ -30,8 +30,17 @@ function runCli(args, environment = {}) { function makeItoProbe(exitCode = 0) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-ito-cli-")); const log = path.join(directory, "invocation.json"); - const script = path.join(directory, "ito-probe.js"); + const script = path.join( + directory, + "ito-cloud-runtime", + "cli", + "ito-compute-cli", + "dist", + "bin", + "ito.js" + ); const executable = script; + fs.mkdirSync(path.dirname(script), { recursive: true }); fs.writeFileSync( script, [ @@ -230,6 +239,71 @@ function main() { fs.rmSync(collisionDirectory, { recursive: true, force: true }); } }], + ["rejects an absolute POSIX shim before it can resolve an interpreter through PATH", () => { + if (process.platform === "win32") return; + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-hostile-ito-shim-")); + const shim = path.join(directory, "ito"); + const hostileNode = path.join(directory, "node"); + const stolenEnvironment = path.join(directory, "stolen.json"); + try { + fs.writeFileSync(shim, "#!/usr/bin/env node\n"); + fs.writeFileSync( + hostileNode, + [ + "#!/bin/sh", + `env > ${JSON.stringify(stolenEnvironment)}`, + "", + ].join("\n") + ); + fs.chmodSync(shim, 0o755); + fs.chmodSync(hostileNode, 0o755); + + const result = runCli(["ito", "auth"], { + ECC_ITO_CLI_EXECUTABLE: shim, + ITO_API_KEY: "must-never-reach-shim-interpreter", + PATH: directory, + }); + + assert.notStrictEqual(result.status, 0); + assert.match(result.stderr, /canonical dist\/bin\/ito\.js/i); + assert.ok( + !fs.existsSync(stolenEnvironment), + "a shim-resolved interpreter must never receive the Itô credential" + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }], + ["rejects a readable JavaScript decoy outside the canonical package entry", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-hostile-ito-js-")); + const decoy = path.join(directory, "ito.js"); + const stolenEnvironment = path.join(directory, "stolen.json"); + try { + fs.writeFileSync( + decoy, + [ + '"use strict";', + 'const fs = require("fs");', + `fs.writeFileSync(${JSON.stringify(stolenEnvironment)}, JSON.stringify(process.env));`, + "", + ].join("\n") + ); + + const result = runCli(["ito", "auth"], { + ECC_ITO_CLI_EXECUTABLE: decoy, + ITO_API_KEY: "must-never-reach-js-decoy", + }); + + assert.notStrictEqual(result.status, 0); + assert.match(result.stderr, /canonical dist\/bin\/ito\.js/i); + assert.ok( + !fs.existsSync(stolenEnvironment), + "an arbitrary JavaScript file must never receive the Itô credential" + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }], ["rejects a relative executable override instead of searching or guessing", () => { const result = runCli(["ito", "status"], { ECC_ITO_CLI_EXECUTABLE: "ito",