From 5a7c06bd0c59b1a489a26f2d45a3034fee06a177 Mon Sep 17 00:00:00 2001 From: Dremig <1466140007@qq.com> Date: Fri, 15 May 2026 18:51:02 +0800 Subject: [PATCH] Fix command injection in yarn metadata lookup --- src/index.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/index.test.ts b/src/index.test.ts index 8c5d12a..2bcf971 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,5 @@ import * as mockfs from "fs" +import * as child_process from "child_process" jest.mock("node-fetch", () => () => Promise.resolve({ ok: true, @@ -13,15 +14,35 @@ import yarn, { checkForRelease, checkForTypesInDeps, getNPMMetadataForDep, + getYarnMetadataForDep, } from "./index" declare const global: any +const RealDate = Date +const fixedDateISOString = "2022-05-15T00:00:00.000Z" +class MockDate extends RealDate { + constructor(...args) { + if (args.length === 0) { + super(fixedDateISOString) + } else { + super(...args) + } + } + static now() { + return new RealDate(fixedDateISOString).getTime() + } +} beforeEach(() => { global.warn = jest.fn() global.message = jest.fn() global.fail = jest.fn() global.markdown = jest.fn() global.danger = { utils: { sentence: jest.fn() } } + global.Date = MockDate + jest.spyOn(child_process, "execFile").mockImplementation(((file, args, callback) => { + callback(null, `{"type":"activityEnd","data":{"id":0}}\n{"type":"info","data":""}`, "") + return {} as any + }) as any) }) afterEach(() => { @@ -29,6 +50,11 @@ afterEach(() => { global.message = undefined global.fail = undefined global.markdown = undefined + global.Date = RealDate + const execFileSpy = child_process.execFile as any + if (execFileSpy.mockRestore) { + execFileSpy.mockRestore() + } }) describe("checkForRelease", () => { @@ -128,6 +154,29 @@ describe("npm metadata", () => { }) }) +describe("yarn metadata", () => { + it("passes dependency names to yarn why without using a shell command string", async () => { + const dep = "danger'; touch SUCCESS; #" + const execFileSpy = child_process.execFile as any + execFileSpy.mockClear() + execFileSpy.mockImplementation(((file, args, callback) => { + expect(file).toBe(process.platform === "win32" ? "yarn.cmd" : "yarn") + expect(args).toEqual(["why", dep, "--json"]) + callback( + null, + `{"type":"activityEnd","data":{"id":0}}\n{"type":"info","data":"Found why output"}`, + "" + ) + return {} as any + }) as any) + + const result = await getYarnMetadataForDep(dep) + + expect(execFileSpy).toHaveBeenCalledTimes(1) + expect(result).toContain("Found why output") + }) +}) + describe("Feature Flags", () => { it("should skip checkForRelease if options.disableCheckForRelease is provided", async () => { await _operateOnSingleDiff( diff --git a/src/index.ts b/src/index.ts index 7f50dd6..928387f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,7 +99,8 @@ export const findNewDependencies = (packageDiff: JSONDiff) => { export const getYarnMetadataForDep = async dep => { return new Promise(resolve => { - child_process.exec(`yarn why '${dep}' --json`, (err, output) => { + const yarnExecutable = process.platform === "win32" ? "yarn.cmd" : "yarn" + child_process.execFile(yarnExecutable, ["why", dep, "--json"], (err, output) => { if (output) { // Comes as a series of little JSON messages const usefulJSONContents = output.toString().split(`{"type":"activityEnd","data":{"id":0}}`).pop() as string