diff --git a/src/__tests__/gf-cli.test.ts b/src/__tests__/gf-cli.test.ts index 5757e2d..fad57cd 100644 --- a/src/__tests__/gf-cli.test.ts +++ b/src/__tests__/gf-cli.test.ts @@ -155,6 +155,36 @@ describe('gfMrCreate', () => { expect(cmd).toContain("'it'\\''s a\ndescription'"); }); + it('should shell-quote repo/branch args to prevent argument injection into bash -c', () => { + mockSpawnSync.mockReturnValue({ + stdout: 'https://git.woa.com/team/repo/-/merge_requests/3', + stderr: '', + status: 0, + } as any); + + gfMrCreate({ + // Values with shell metacharacters that would break out of the command + // if interpolated raw into `bash -c " "`. + repo: 'team/repo;echo PWNED', + source: 'feat;rm -rf / #', + target: 'master|cat /etc/passwd', + title: 't', + }); + + const cmd = mockSpawnSync.mock.calls[0][1]![1] as string; + // Each dangerous value must be a single single-quoted token so bash -c + // treats its metacharacters (`;`, `|`, `#`, spaces) as literal argument + // content rather than command separators. + expect(cmd).toContain("'team/repo;echo PWNED'"); + expect(cmd).toContain("'feat;rm -rf / #'"); + expect(cmd).toContain("'master|cat /etc/passwd'"); + // No bare (unquoted) `;` that bash could interpret as a command separator: + // every `;` must sit inside a single-quoted token. + const outsideQuotes = cmd.replace(/'[^']*'/g, ''); + expect(outsideQuotes).not.toContain(';'); + expect(outsideQuotes).not.toContain('|'); + }); + it('should return MR URL from gf output', () => { mockSpawnSync.mockReturnValue({ stdout: 'Created: https://git.woa.com/team/repo/-/merge_requests/42', diff --git a/src/providers/tgit/gf-cli.ts b/src/providers/tgit/gf-cli.ts index 84b32de..8c3cc0c 100644 --- a/src/providers/tgit/gf-cli.ts +++ b/src/providers/tgit/gf-cli.ts @@ -35,7 +35,11 @@ export function gfExec( options?: { inheritStdio?: boolean; cwd?: string }, ): { stdout: string; stderr: string; status: number } { const gfPath = getGfPath(); - const cmd = `${gfPath} ${args.join(' ')}`; + // Shell-quote every token (including the binary path) so values such as repo + // paths, branch names, titles, and descriptions cannot inject shell + // metacharacters (`;`, `|`, `$()`, …) into the `bash -c` string. Callers pass + // raw values; quoting centrally here keeps every entry point safe. + const cmd = [shellQuote(gfPath), ...args.map(shellQuote)].join(' '); log.debug(`gf exec: ${cmd}`); if (options?.inheritStdio) { @@ -398,11 +402,11 @@ export function gfMrCreate(opts: GfMrCreateOptions): string { '-R', opts.repo, '-s', opts.source, '-T', opts.target, - '-t', shellQuote(opts.title), + '-t', opts.title, ]; if (opts.description) { - args.push('-d', shellQuote(opts.description)); + args.push('-d', opts.description); } if (opts.reviewers && opts.reviewers.length > 0) {