mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix: validate CCS Bar launch shim target integrity (#1626)
* fix: validate CCS Bar launch shim target integrity * fix: execute verified bar shim bytes
This commit is contained in:
1 parent
05685d1c21
commit
3621f8dcb1
2 files changed
+78
-3
No files matched your search
@@ -8,6 +8,7 @@
|
|||||||
* instead of the package-manager entrypoint.
|
* instead of the package-manager entrypoint.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -17,6 +18,10 @@ import type { LaunchJson } from './bar-paths';
|
|||||||
|
|
||||||
const SHIM_MODE = 0o700;
|
const SHIM_MODE = 0o700;
|
||||||
|
|
||||||
|
function sha256File(filePath: string): string {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
export interface LaunchDescriptorOptions {
|
export interface LaunchDescriptorOptions {
|
||||||
entrypointPath?: string;
|
entrypointPath?: string;
|
||||||
runtime?: string;
|
runtime?: string;
|
||||||
@@ -38,11 +43,29 @@ function resolveEntrypoint(entrypointPath?: string): string {
|
|||||||
|
|
||||||
export function writeLaunchShim(home: string, entrypointPath?: string): string {
|
export function writeLaunchShim(home: string, entrypointPath?: string): string {
|
||||||
const resolvedEntrypoint = resolveEntrypoint(entrypointPath);
|
const resolvedEntrypoint = resolveEntrypoint(entrypointPath);
|
||||||
|
const expectedEntrypointHash = sha256File(resolvedEntrypoint);
|
||||||
const shimPath = getLaunchShimPath(home);
|
const shimPath = getLaunchShimPath(home);
|
||||||
const shimDir = path.dirname(shimPath);
|
const shimDir = path.dirname(shimPath);
|
||||||
const contents = [
|
const contents = [
|
||||||
'#!/usr/bin/env node',
|
'#!/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');
|
].join('\n');
|
||||||
|
|
||||||
|
|||||||
@@ -805,9 +805,61 @@ describe('launch descriptor shim', () => {
|
|||||||
const mode = fs.statSync(descriptor.args[0]).mode & 0o777;
|
const mode = fs.statSync(descriptor.args[0]).mode & 0o777;
|
||||||
expect((mode & 0o022) === 0).toBe(true);
|
expect((mode & 0o022) === 0).toBe(true);
|
||||||
const resolvedEntrypoint = fs.realpathSync(realEntrypoint);
|
const resolvedEntrypoint = fs.realpathSync(realEntrypoint);
|
||||||
expect(fs.readFileSync(descriptor.args[0], 'utf8')).toContain(
|
const shimContents = fs.readFileSync(descriptor.args[0], 'utf8');
|
||||||
`require(${JSON.stringify(resolvedEntrypoint)});`
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user