From dc575f168bebdb25b0797ea1815acc3476a982a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:01:19 +0000 Subject: [PATCH 1/3] Initial plan From d9b1cd511d5c2eccdf568511576c3bbb64ac097c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:10:54 +0000 Subject: [PATCH 2/3] Fix ReDoS vulnerability in secret scanning regex patterns Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/redact_secrets.cjs | 4 +- actions/setup/js/redact_secrets.test.cjs | 78 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index 882a90d6f49..db2f172b710 100644 --- a/actions/setup/js/redact_secrets.cjs +++ b/actions/setup/js/redact_secrets.cjs @@ -55,11 +55,11 @@ const BUILT_IN_PATTERNS = [ // Azure tokens { name: "Azure Storage Account Key", pattern: /[a-zA-Z0-9+/]{88}==/g }, - { name: "Azure SAS Token", pattern: /\?sv=[0-9-]+&s[rts]=[\w\-]+&sig=[A-Za-z0-9%+/=]+/g }, + { name: "Azure SAS Token", pattern: /\?sv=[0-9-]{1,20}&s[rts]=[\w\-]{1,20}&sig=[A-Za-z0-9%+/=]{1,200}/g }, // Google/GCP tokens { name: "Google API Key", pattern: /AIzaSy[0-9A-Za-z_-]{33}/g }, - { name: "Google OAuth Access Token", pattern: /ya29\.[0-9A-Za-z_-]+/g }, + { name: "Google OAuth Access Token", pattern: /ya29\.[0-9A-Za-z_-]{1,800}/g }, // AWS tokens { name: "AWS Access Key ID", pattern: /AKIA[0-9A-Z]{16}/g }, diff --git a/actions/setup/js/redact_secrets.test.cjs b/actions/setup/js/redact_secrets.test.cjs index a49f7dc58c6..2649663f206 100644 --- a/actions/setup/js/redact_secrets.test.cjs +++ b/actions/setup/js/redact_secrets.test.cjs @@ -452,6 +452,84 @@ Custom secret: my-secret-123456789012`; expect(redacted).toContain("AKI*****************"); expect(redacted).toContain("my-*******************"); }); + + describe("ReDoS protection", () => { + it("should handle pathological Azure SAS Token input without timing out", async () => { + const testFile = path.join(tempDir, "test.txt"); + // Create pathological input that would cause ReDoS with unbounded quantifiers + const pathological = `?sv=${"9".repeat(1000)}&srt=${"w".repeat(1000)}&sig=${"A".repeat(1000)}`; + fs.writeFileSync(testFile, `Pathological: ${pathological}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + + // This should complete quickly (< 1 second) without hanging + const startTime = Date.now(); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const duration = Date.now() - startTime; + + // Verify it completed quickly (should be < 1000ms, but allow 5000ms for slower CI) + expect(duration).toBeLessThan(5000); + + // The pattern shouldn't match due to length bounds + const content = fs.readFileSync(testFile, "utf8"); + expect(content).toBe(`Pathological: ${pathological}`); + }); + + it("should handle pathological Google OAuth token input without timing out", async () => { + const testFile = path.join(tempDir, "test.txt"); + // Create pathological input that would cause ReDoS with unbounded quantifiers + const pathological = `ya29.${"A".repeat(5000)}`; + fs.writeFileSync(testFile, `Token: ${pathological}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + + // This should complete quickly (< 1 second) without hanging + const startTime = Date.now(); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const duration = Date.now() - startTime; + + // Verify it completed quickly (should be < 1000ms, but allow 5000ms for slower CI) + expect(duration).toBeLessThan(5000); + + // The pattern should match up to 800 chars and redact it + const content = fs.readFileSync(testFile, "utf8"); + expect(content).toContain("ya2"); + expect(content).not.toBe(`Token: ${pathological}`); + // Should contain asterisks from redaction + expect(content).toContain("*"); + // Should still have unredacted 'A' chars at the end beyond 800 char limit + expect(content).toMatch(/\*+A+$/); + }); + + it("should still match valid Azure SAS tokens within bounds", async () => { + const testFile = path.join(tempDir, "test.txt"); + // Valid Azure SAS token within bounds + const validSAS = "?sv=2021-06-08&sr=b&sig=AbCdEf0123456789+/="; + fs.writeFileSync(testFile, `SAS: ${validSAS}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + // Should be redacted since it's a valid pattern within bounds + expect(redacted).toContain("?sv"); + expect(redacted).not.toBe(`SAS: ${validSAS}`); + }); + + it("should still match valid Google OAuth tokens within bounds", async () => { + const testFile = path.join(tempDir, "test.txt"); + // Valid Google OAuth token within bounds (typical length ~100-200 chars) + const validToken = "ya29." + "a".repeat(150); + fs.writeFileSync(testFile, `Token: ${validToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + // Should be redacted since it's a valid pattern within bounds + expect(redacted).toContain("ya2"); + expect(redacted).not.toBe(`Token: ${validToken}`); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Google OAuth Access Token")); + }); + }); }); })); }); From 75b6343b3d434c55bb88c2ba0d760ec8f920508d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:45:28 +0000 Subject: [PATCH 3/3] Fix tests after merging main - update assertions for new redaction format Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/redact_secrets.test.cjs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/redact_secrets.test.cjs b/actions/setup/js/redact_secrets.test.cjs index faeb1b9a337..22690da01dc 100644 --- a/actions/setup/js/redact_secrets.test.cjs +++ b/actions/setup/js/redact_secrets.test.cjs @@ -106,7 +106,7 @@ describe("redact_secrets.cjs", () => { const secretValue = "abc123"; (fs.writeFileSync(testFile, `Secret: ${secretValue} test`), (process.env.GH_AW_SECRET_NAMES = "SIX_CHAR_SECRET"), (process.env.SECRET_SIX_CHAR_SECRET = secretValue)); const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); - (await eval(`(async () => { ${modifiedScript}; await main(); })()`), expect(fs.readFileSync(testFile, "utf8")).toBe("Secret: abc*** test")); + (await eval(`(async () => { ${modifiedScript}; await main(); })()`), expect(fs.readFileSync(testFile, "utf8")).toBe("Secret: ***REDACTED*** test")); }), it("should handle multiple secrets in same file", async () => { const testFile = path.join(tempDir, "test.txt"), @@ -500,12 +500,10 @@ Custom secret: my-secret-123456789012`; // The pattern should match up to 800 chars and redact it const content = fs.readFileSync(testFile, "utf8"); - expect(content).toContain("ya2"); + expect(content).toContain("***REDACTED***"); expect(content).not.toBe(`Token: ${pathological}`); - // Should contain asterisks from redaction - expect(content).toContain("*"); // Should still have unredacted 'A' chars at the end beyond 800 char limit - expect(content).toMatch(/\*+A+$/); + expect(content).toMatch(/\*\*\*REDACTED\*\*\*A+$/); }); it("should still match valid Azure SAS tokens within bounds", async () => { @@ -518,8 +516,7 @@ Custom secret: my-secret-123456789012`; await eval(`(async () => { ${modifiedScript}; await main(); })()`); const redacted = fs.readFileSync(testFile, "utf8"); // Should be redacted since it's a valid pattern within bounds - expect(redacted).toContain("?sv"); - expect(redacted).not.toBe(`SAS: ${validSAS}`); + expect(redacted).toBe("SAS: ***REDACTED***"); }); it("should still match valid Google OAuth tokens within bounds", async () => { @@ -532,8 +529,7 @@ Custom secret: my-secret-123456789012`; await eval(`(async () => { ${modifiedScript}; await main(); })()`); const redacted = fs.readFileSync(testFile, "utf8"); // Should be redacted since it's a valid pattern within bounds - expect(redacted).toContain("ya2"); - expect(redacted).not.toBe(`Token: ${validToken}`); + expect(redacted).toBe("Token: ***REDACTED***"); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Google OAuth Access Token")); }); });