diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index 972eb6c0b0f..738cc745a9f 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 09d0905dc35..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"), @@ -459,6 +459,80 @@ Custom secret: my-secret-123456789012`; expect(redacted).toContain("***REDACTED***"); expect(redacted).toContain("***REDACTED***"); }); + + 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("***REDACTED***"); + expect(content).not.toBe(`Token: ${pathological}`); + // Should still have unredacted 'A' chars at the end beyond 800 char limit + expect(content).toMatch(/\*\*\*REDACTED\*\*\*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).toBe("SAS: ***REDACTED***"); + }); + + 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).toBe("Token: ***REDACTED***"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Google OAuth Access Token")); + }); + }); }); })); });