Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions actions/setup/js/validate_lockdown_requirements.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
79 changes: 79 additions & 0 deletions actions/setup/js/validate_lockdown_requirements.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
});
});
});