From 3621f8dcb1454cc9c6d25669cdd46f227282fbe7 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:49:56 -0400 Subject: [PATCH] fix: validate CCS Bar launch shim target integrity (#1626) * fix: validate CCS Bar launch shim target integrity * fix: execute verified bar shim bytes --- src/commands/bar/launch-descriptor.ts | 25 ++++++++- .../bar-lifecycle-subcommands.test.ts | 56 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/commands/bar/launch-descriptor.ts b/src/commands/bar/launch-descriptor.ts index 61ededb0..71e3753e 100644 --- a/src/commands/bar/launch-descriptor.ts +++ b/src/commands/bar/launch-descriptor.ts @@ -8,6 +8,7 @@ * instead of the package-manager entrypoint. */ +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -17,6 +18,10 @@ import type { LaunchJson } from './bar-paths'; const SHIM_MODE = 0o700; +function sha256File(filePath: string): string { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); +} + export interface LaunchDescriptorOptions { entrypointPath?: string; runtime?: string; @@ -38,11 +43,29 @@ function resolveEntrypoint(entrypointPath?: string): string { export function writeLaunchShim(home: string, entrypointPath?: string): string { const resolvedEntrypoint = resolveEntrypoint(entrypointPath); + const expectedEntrypointHash = sha256File(resolvedEntrypoint); const shimPath = getLaunchShimPath(home); const shimDir = path.dirname(shimPath); const contents = [ '#!/usr/bin/env node', - `require(${JSON.stringify(resolvedEntrypoint)});`, + "const crypto = require('crypto');", + "const fs = require('fs');", + "const Module = require('module');", + "const path = require('path');", + `const expectedEntrypoint = ${JSON.stringify(resolvedEntrypoint)};`, + `const expectedHash = ${JSON.stringify(expectedEntrypointHash)};`, + 'const resolvedEntrypoint = fs.realpathSync(expectedEntrypoint);', + "if (resolvedEntrypoint !== expectedEntrypoint) throw new Error('CCS Bar launch shim target changed. Run `ccs bar launch` to refresh launch.json.');", + 'const entrypointStat = fs.statSync(resolvedEntrypoint);', + "if (!entrypointStat.isFile()) throw new Error('CCS Bar launch shim target is not a regular file.');", + 'const source = fs.readFileSync(resolvedEntrypoint);', + "const actualHash = crypto.createHash('sha256').update(source).digest('hex');", + "if (actualHash !== expectedHash) throw new Error('CCS Bar launch shim target changed. Run `ccs bar launch` to refresh launch.json.');", + 'const targetModule = new Module(resolvedEntrypoint, module);', + 'targetModule.filename = resolvedEntrypoint;', + 'targetModule.paths = Module._nodeModulePaths(path.dirname(resolvedEntrypoint));', + 'require.cache[resolvedEntrypoint] = targetModule;', + "targetModule._compile(source.toString('utf8'), resolvedEntrypoint);", '', ].join('\n'); diff --git a/tests/unit/commands/bar-lifecycle-subcommands.test.ts b/tests/unit/commands/bar-lifecycle-subcommands.test.ts index d7a4d285..fdebbc2b 100644 --- a/tests/unit/commands/bar-lifecycle-subcommands.test.ts +++ b/tests/unit/commands/bar-lifecycle-subcommands.test.ts @@ -805,9 +805,61 @@ describe('launch descriptor shim', () => { const mode = fs.statSync(descriptor.args[0]).mode & 0o777; expect((mode & 0o022) === 0).toBe(true); const resolvedEntrypoint = fs.realpathSync(realEntrypoint); - expect(fs.readFileSync(descriptor.args[0], 'utf8')).toContain( - `require(${JSON.stringify(resolvedEntrypoint)});` + const shimContents = fs.readFileSync(descriptor.args[0], 'utf8'); + expect(shimContents).toContain( + `const expectedEntrypoint = ${JSON.stringify(resolvedEntrypoint)};` ); + expect(shimContents).toContain('const expectedHash = '); + expect(shimContents).toContain('const source = fs.readFileSync(resolvedEntrypoint);'); + expect(shimContents).toContain('if (actualHash !== expectedHash)'); + expect(shimContents).toContain('require.cache[resolvedEntrypoint] = targetModule;'); + expect(shimContents).toContain( + "targetModule._compile(source.toString('utf8'), resolvedEntrypoint);" + ); + expect(shimContents).not.toContain('require(resolvedEntrypoint);'); + }); + + it('rejects a modified original entrypoint before executing it', async () => { + const packageDist = path.join( + tempHome, + '.bun', + 'install', + 'global', + 'node_modules', + '@kaitranntt', + 'ccs', + 'dist' + ); + const binDir = path.join(tempHome, '.bun', 'bin'); + const markerPath = path.join(tempHome, 'attacker-marker'); + const realEntrypoint = path.join(packageDist, 'ccs.js'); + const symlinkedEntrypoint = path.join(binDir, 'ccs'); + + fs.mkdirSync(packageDist, { recursive: true }); + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync(realEntrypoint, 'console.log("original");\n', { mode: 0o777 }); + fs.symlinkSync(realEntrypoint, symlinkedEntrypoint); + + const { createBarLaunchDescriptor } = await loadLaunchDescriptor(); + const descriptor = createBarLaunchDescriptor({ + entrypointPath: symlinkedEntrypoint, + runtime: process.execPath, + home: tempHome, + }); + + fs.writeFileSync( + realEntrypoint, + `require('fs').writeFileSync(${JSON.stringify(markerPath)}, 'executed');\n` + ); + + const proc = Bun.spawnSync([descriptor.runtime, ...descriptor.args], { + stdout: 'pipe', + stderr: 'pipe', + }); + + expect(proc.exitCode).not.toBe(0); + expect(Buffer.from(proc.stderr).toString()).toContain('CCS Bar launch shim target changed'); + expect(fs.existsSync(markerPath)).toBe(false); }); });