mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-05 10: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.
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user