Files
nexu-io--open-design/apps/daemon/tests/host-tools-launch-shell.test.ts
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

65 lines
2.7 KiB
TypeScript

import { EventEmitter } from 'node:events';
import { afterEach, describe, expect, it, vi } from 'vitest';
// Capture spawn options without launching anything. The child emits `spawn` on
// the next tick so launchHostTool resolves { ok: true }.
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>();
return {
...actual,
spawn: vi.fn(() => {
const child = new EventEmitter() as EventEmitter & { unref: () => void };
child.unref = () => {};
setImmediate(() => child.emit('spawn'));
return child;
}),
};
});
import { spawn } from 'node:child_process';
import { launchHostTool } from '../src/routes/host-tools.js';
const spawnMock = spawn as unknown as ReturnType<typeof vi.fn>;
// Regression: launchHostTool used `spawn(command, args, { shell: process.platform === 'win32' })`.
// On Windows that runs the launch through cmd.exe, which shell-interprets the
// args — one of which is the (project-derived) directory path — so a path with
// cmd metacharacters (`&`, `|`, `^`, `>`) could inject commands. The fix routes
// through `createCommandInvocation` (no shell; .cmd/.bat via cmd.exe with
// verbatim, CommandLineToArgvW-safe args).
describe('launchHostTool does not launch through a shell', () => {
const origPlatform = process.platform;
afterEach(() => {
Object.defineProperty(process, 'platform', { value: origPlatform, configurable: true });
vi.clearAllMocks();
});
it('never passes shell:true for a Windows .cmd editor (no command injection via the dir arg)', async () => {
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
const result = await launchHostTool('C:\\tools\\code.cmd', ['C:\\Users\\me\\proj & calc.exe']);
expect(result).toEqual({ ok: true });
const opts = spawnMock.mock.calls[0]?.[2] ?? {};
// The vulnerable version set `shell: true` here.
expect(opts.shell).not.toBe(true);
// Routed through createCommandInvocation: a .cmd runs via cmd.exe with
// verbatim args (Node must not re-quote them), never a raw shell line.
expect(opts.windowsVerbatimArguments).toBe(true);
});
it('spawns a non-Windows launch without a shell and passes the dir as a literal arg', async () => {
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
const dir = '/home/me/proj & rm -rf ~';
const result = await launchHostTool('/usr/bin/code', [dir]);
expect(result).toEqual({ ok: true });
const [, args, opts] = spawnMock.mock.calls[0]!;
expect(opts.shell).toBeFalsy();
// No shell → the metacharacter-laden path is one argv element, inert.
expect(args).toContain(dir);
});
});