From a45cab95b18a4934603302604cb02d1e959a2321 Mon Sep 17 00:00:00 2001 From: Dremig <1466140007@qq.com> Date: Sat, 16 May 2026 18:24:42 +0800 Subject: [PATCH] Avoid shell execution in create-boilerplate --- .../__tests__/create-boilerplate.test.ts | 85 +++++++++++++++++++ .../src/commands/create-boilerplate.ts | 11 ++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 packages/ts-codegen/__tests__/create-boilerplate.test.ts diff --git a/packages/ts-codegen/__tests__/create-boilerplate.test.ts b/packages/ts-codegen/__tests__/create-boilerplate.test.ts new file mode 100644 index 00000000..9783a9bb --- /dev/null +++ b/packages/ts-codegen/__tests__/create-boilerplate.test.ts @@ -0,0 +1,85 @@ +import { spawnSync } from 'child_process'; +import { lstatSync, readFileSync, writeFileSync } from 'fs'; +import { globSync as glob } from 'glob'; +import * as shell from 'shelljs'; + +import createBoilerplate from '../src/commands/create-boilerplate'; +import { prompt } from '../src/utils/prompt'; + +jest.mock('child_process', () => ({ + spawnSync: jest.fn(), +})); + +jest.mock('fs', () => ({ + lstatSync: jest.fn(), + readFileSync: jest.fn(), + writeFileSync: jest.fn(), +})); + +jest.mock('glob', () => ({ + globSync: jest.fn(), +})); + +jest.mock('shelljs', () => ({ + which: jest.fn(), + echo: jest.fn(), + exit: jest.fn(), + exec: jest.fn(), + cd: jest.fn(), + rm: jest.fn(), +})); + +jest.mock('../src/utils/prompt', () => ({ + prompt: jest.fn(), +})); + +describe('create-boilerplate', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('treats name as a git argument instead of shell-interpreted text', async () => { + const payload = 'test; id > /tmp/pwned123456 #'; + + (shell.which as jest.Mock).mockReturnValue(true); + (shell.exec as jest.Mock) + .mockReturnValueOnce('Alice Example') + .mockReturnValueOnce('alice@example.com'); + (spawnSync as jest.Mock).mockReturnValue({ status: 0 }); + (prompt as jest.Mock) + .mockResolvedValueOnce({ name: payload }) + .mockResolvedValueOnce({ + __ACCESS__: 'private', + __USERNAME__: 'alice', + __MODULENAME__: 'module', + }) + .mockResolvedValueOnce({ __LICENSE__: 'MIT' }); + (glob as jest.Mock) + .mockReturnValueOnce([]) + .mockReturnValueOnce(['template.txt']); + (lstatSync as jest.Mock).mockReturnValue({ + isDirectory: () => false, + }); + (readFileSync as jest.Mock).mockImplementation((filePath: string) => { + if (filePath === '.questions.json') { + return '[]'; + } + return '__MODULENAME__ __PACKAGE_IDENTIFIER__'; + }); + + await createBoilerplate({}); + + expect(spawnSync).toHaveBeenCalledWith( + 'git', + [ + 'clone', + 'https://github.com/hyperweb-io/ts-codegen-module-boilerplate', + payload, + ], + { stdio: 'inherit' } + ); + expect(shell.exec).toHaveBeenCalledTimes(2); + expect(shell.cd).toHaveBeenCalledWith(payload); + expect(writeFileSync).toHaveBeenCalled(); + }); +}); diff --git a/packages/ts-codegen/src/commands/create-boilerplate.ts b/packages/ts-codegen/src/commands/create-boilerplate.ts index cec4fb2d..ec923580 100644 --- a/packages/ts-codegen/src/commands/create-boilerplate.ts +++ b/packages/ts-codegen/src/commands/create-boilerplate.ts @@ -1,4 +1,5 @@ import { MinimistArgs } from '@cosmwasm/ts-codegen-types'; +import { spawnSync } from 'child_process'; import dargs from 'dargs'; import { lstatSync, readFileSync, writeFileSync } from 'fs'; import { globSync as glob } from 'glob'; @@ -25,7 +26,15 @@ export default async (argv: MinimistArgs) => { argv ); - shell.exec(`git clone ${repo} ${name}`); + const cloneResult = spawnSync('git', ['clone', repo, name], { + stdio: 'inherit', + }); + if (cloneResult.error) { + throw cloneResult.error; + } + if (cloneResult.status !== 0) { + return shell.exit(cloneResult.status ?? 1); + } shell.cd(name); const questions = JSON.parse(readFileSync(`.questions.json`, 'utf-8'));