diff --git a/actions/setup/js/validate_lockdown_requirements.cjs b/actions/setup/js/validate_lockdown_requirements.cjs index 992f38dee4c..14f74a99b30 100644 --- a/actions/setup/js/validate_lockdown_requirements.cjs +++ b/actions/setup/js/validate_lockdown_requirements.cjs @@ -12,6 +12,10 @@ * strict mode enabled (GH_AW_COMPILED_STRICT=true). This ensures that public * repository workflows meet the security requirements enforced by strict mode. * + * Finally, the pull_request_target event is disallowed on public repositories + * to prevent "pwn request" attacks where a fork can trigger workflows with access + * to repository secrets. + * * This validation runs at the start of the workflow to fail fast if requirements * are not met, providing clear guidance to the user. * @@ -92,6 +96,29 @@ function validateLockdownRequirements(core) { if (isPublic && isStrict) { core.info("✓ Strict mode requirements validated: Public repository compiled with strict mode"); } + + // Disallow pull_request_target event in public repositories. + // The pull_request_target event runs workflows in the context of the base repository + // with access to secrets, even when triggered from a fork. This creates a significant + // security risk in public repositories where anyone can open a pull request from a fork + // and potentially exfiltrate secrets or cause unintended side effects. + const eventName = process.env.GITHUB_EVENT_NAME; + if (isPublic && eventName === "pull_request_target") { + const errorMessage = + "This workflow is triggered by the pull_request_target event on a public repository.\\n" + + "\\n" + + "The pull_request_target event is not allowed on public repositories because it runs\\n" + + "workflows with access to repository secrets even when triggered from a fork, which\\n" + + 'creates a significant security risk (known as a "pwn request").\\n' + + "\\n" + + "To fix this, use the pull_request event instead, or migrate to a private repository.\\n" + + "\\n" + + "See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx"; + + core.setOutput("lockdown_check_failed", "true"); + core.setFailed(errorMessage); + throw new Error(errorMessage); + } } module.exports = validateLockdownRequirements; diff --git a/actions/setup/js/validate_lockdown_requirements.test.cjs b/actions/setup/js/validate_lockdown_requirements.test.cjs index 3d248c58675..e6e2a323ad1 100644 --- a/actions/setup/js/validate_lockdown_requirements.test.cjs +++ b/actions/setup/js/validate_lockdown_requirements.test.cjs @@ -21,6 +21,7 @@ describe("validate_lockdown_requirements", () => { delete process.env.CUSTOM_GITHUB_TOKEN; delete process.env.GITHUB_REPOSITORY_VISIBILITY; delete process.env.GH_AW_COMPILED_STRICT; + delete process.env.GITHUB_EVENT_NAME; // Import the module validateLockdownRequirements = (await import("./validate_lockdown_requirements.cjs")).default; @@ -225,4 +226,82 @@ describe("validate_lockdown_requirements", () => { expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Lockdown mode is enabled")); }); }); + + // pull_request_target event enforcement for public repositories + describe("pull_request_target event enforcement for public repositories", () => { + it("should fail when repository is public and event is pull_request_target", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "true"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow("pull_request_target"); + + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("pull_request_target event on a public repository")); + expect(mockCore.setOutput).toHaveBeenCalledWith("lockdown_check_failed", "true"); + }); + + it("should pass when repository is public but event is pull_request (not pull_request_target)", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "true"; + process.env.GITHUB_EVENT_NAME = "pull_request"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should pass when repository is private and event is pull_request_target", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "private"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should pass when repository is internal and event is pull_request_target", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "internal"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should pass when event is pull_request_target but visibility is unknown", () => { + // GITHUB_REPOSITORY_VISIBILITY not set + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should include security documentation link in pull_request_target error message", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "true"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow(); + + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx")); + }); + + it("should fail on strict mode check before pull_request_target check when both fail", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "false"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow("not compiled with strict mode"); + + // pull_request_target error should not be reached since strict mode check throws first + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("not compiled with strict mode")); + }); + }); });