-
Notifications
You must be signed in to change notification settings - Fork 218
[Safe Outputs] Add max limit enforcement to 7 core handlers (SEC-003) #15806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
99c9056
023f309
8efb505
6bacee9
14bc93d
3953c8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -478,5 +478,35 @@ describe("add_labels", () => { | |||||
| expect(addLabelsCalls[0].owner).toBe("github"); | ||||||
| expect(addLabelsCalls[0].repo).toBe("gh-aw"); | ||||||
| }); | ||||||
|
|
||||||
| it("should enforce max limit on labels per operation", async () => { | ||||||
| const handler = await main({ max: 10 }); | ||||||
|
|
||||||
| // Try to add more than MAX_LABELS (10) | ||||||
| const result = await handler( | ||||||
| { | ||||||
| item_number: 100, | ||||||
| labels: [ | ||||||
| "label1", | ||||||
| "label2", | ||||||
| "label3", | ||||||
| "label4", | ||||||
| "label5", | ||||||
| "label6", | ||||||
| "label7", | ||||||
| "label8", | ||||||
| "label9", | ||||||
| "label10", | ||||||
| "label11", // 11th label exceeds limit | ||||||
| ], | ||||||
| }, | ||||||
| {} | ||||||
| ); | ||||||
|
|
||||||
| expect(result.success).toBe(false); | ||||||
| expect(result.error).toContain("E003"); | ||||||
|
||||||
| expect(result.error).toContain("E003"); | |
| expect(result.error).toContain("E002"); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -392,4 +392,40 @@ describe("create_discussion with labels", () => { | |||||
| expect(labelsApplyCall).toBeDefined(); | ||||||
| expect(labelsApplyCall[1].labelIds).toEqual(["LA_label1", "LA_label2"]); | ||||||
| }); | ||||||
|
|
||||||
| it("should enforce max labels limit (SEC-003)", async () => { | ||||||
| process.env.GH_AW_WORKFLOW_ID = "test-workflow"; | ||||||
| process.env.GH_AW_WORKFLOW_NAME = "Test Workflow"; | ||||||
| process.env.GH_AW_WORKFLOW_SOURCE = "test.md"; | ||||||
|
|
||||||
| const handler = await createDiscussionMain({ | ||||||
| category: "General", | ||||||
| }); | ||||||
|
|
||||||
| // Try to create discussion with more than MAX_LABELS (10) | ||||||
| const message = { | ||||||
| title: "Test Discussion", | ||||||
| body: "This is a test discussion body", | ||||||
| labels: [ | ||||||
| "label1", | ||||||
| "label2", | ||||||
| "label3", | ||||||
| "label4", | ||||||
| "label5", | ||||||
| "label6", | ||||||
| "label7", | ||||||
| "label8", | ||||||
| "label9", | ||||||
| "label10", | ||||||
| "label11", // 11th label exceeds limit | ||||||
| ], | ||||||
| }; | ||||||
|
|
||||||
| const result = await handler(message, {}); | ||||||
|
|
||||||
| expect(result.success).toBe(false); | ||||||
| expect(result.error).toContain("E003"); | ||||||
|
||||||
| expect(result.error).toContain("E003"); | |
| expect(result.error).toContain("E002"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -390,4 +390,48 @@ describe("create_issue", () => { | |
| expect(createCall.body).toContain("Related to #456"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("max limit enforcement", () => { | ||
| it("should enforce max limit on labels", async () => { | ||
| const handler = await main({}); | ||
|
|
||
| const result = await handler({ | ||
| title: "Test Issue", | ||
| body: "Test body", | ||
| labels: [ | ||
| "label1", | ||
| "label2", | ||
| "label3", | ||
| "label4", | ||
| "label5", | ||
| "label6", | ||
| "label7", | ||
| "label8", | ||
| "label9", | ||
| "label10", | ||
| "label11", // 11th label exceeds limit | ||
| ], | ||
| }); | ||
|
|
||
| expect(result.success).toBe(false); | ||
| expect(result.error).toContain("E003"); | ||
| expect(result.error).toContain("Cannot add more than 10 labels"); | ||
| expect(result.error).toContain("received 11"); | ||
|
Comment on lines
+417
to
+419
|
||
| }); | ||
|
|
||
| it("should enforce max limit on assignees", async () => { | ||
| const handler = await main({}); | ||
|
|
||
| const result = await handler({ | ||
| title: "Test Issue", | ||
| body: "Test body", | ||
| assignees: ["user1", "user2", "user3", "user4", "user5", "user6"], // 6 assignees exceeds limit of 5 | ||
| }); | ||
|
|
||
| expect(result.success).toBe(false); | ||
| expect(result.error).toContain("E003"); | ||
| expect(result.error).toContain("Cannot add more than 5 assignees"); | ||
| expect(result.error).toContain("received 6"); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,34 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); | |
| /** @type {string} Safe output type handled by this module */ | ||
| const HANDLER_TYPE = "create_pull_request"; | ||
|
|
||
| /** | ||
| * Maximum limits for pull request parameters to prevent resource exhaustion. | ||
| * These limits align with GitHub's API constraints and security best practices. | ||
| */ | ||
| /** @type {number} Maximum number of files allowed per pull request */ | ||
| const MAX_FILES = 100; | ||
|
|
||
| /** | ||
| * Enforces maximum limits on pull request parameters to prevent resource exhaustion attacks. | ||
| * Per Safe Outputs specification requirement SEC-003, limits must be enforced before API calls. | ||
| * | ||
| * @param {string} patchContent - Patch content to validate | ||
| * @throws {Error} When any limit is exceeded, with error code E003 and details | ||
|
Comment on lines
+34
to
+38
|
||
| */ | ||
| function enforcePullRequestLimits(patchContent) { | ||
| if (!patchContent || !patchContent.trim()) { | ||
| return; | ||
| } | ||
|
|
||
| // Count files in patch by looking for "diff --git" lines | ||
| const fileMatches = patchContent.match(/^diff --git /gm); | ||
| const fileCount = fileMatches ? fileMatches.length : 0; | ||
|
|
||
| // Check file count - max limit exceeded check | ||
| if (fileCount > MAX_FILES) { | ||
| throw new Error(`E003: Cannot create pull request with more than ${MAX_FILES} files (received ${fileCount})`); | ||
| } | ||
| } | ||
| /** | ||
| * Generate a patch preview with max 500 lines and 2000 chars for issue body | ||
| * @param {string} patchContent - The full patch content | ||
|
|
@@ -219,6 +247,15 @@ async function main(config = {}) { | |
| isEmpty = !patchContent || !patchContent.trim(); | ||
| } | ||
|
|
||
| // Enforce max limits on patch before processing | ||
| try { | ||
| enforcePullRequestLimits(patchContent); | ||
| } catch (error) { | ||
| const errorMessage = getErrorMessage(error); | ||
| core.warning(`Pull request limit exceeded: ${errorMessage}`); | ||
| return { success: false, error: errorMessage }; | ||
| } | ||
|
|
||
| // Check for actual error conditions (but allow empty patches as valid noop) | ||
| if (patchContent.includes("Failed to generate patch")) { | ||
| // If allow-empty is enabled, ignore patch errors and proceed | ||
|
|
@@ -859,4 +896,4 @@ You can manually create a pull request from the branch if needed.${patchPreview} | |
| }; // End of handleCreatePullRequest | ||
| } // End of main | ||
|
|
||
| module.exports = { main }; | ||
| module.exports = { main, enforcePullRequestLimits }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||||||
| // @ts-check | ||||||||||
| import { describe, it, expect } from "vitest"; | ||||||||||
| import { describe, it, expect, beforeEach, vi } from "vitest"; | ||||||||||
| import { createRequire } from "module"; | ||||||||||
|
|
||||||||||
| const require = createRequire(import.meta.url); | ||||||||||
|
|
||||||||||
| describe("create_pull_request - fallback-as-issue configuration", () => { | ||||||||||
| describe("configuration parsing", () => { | ||||||||||
|
|
@@ -46,3 +49,58 @@ describe("create_pull_request - fallback-as-issue configuration", () => { | |||||||||
| }); | ||||||||||
| }); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| describe("create_pull_request - max limit enforcement", () => { | ||||||||||
| let mockFs; | ||||||||||
|
|
||||||||||
| beforeEach(() => { | ||||||||||
| // Mock fs module for patch reading | ||||||||||
| mockFs = { | ||||||||||
| existsSync: vi.fn().mockReturnValue(true), | ||||||||||
| readFileSync: vi.fn(), | ||||||||||
| }; | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| it("should enforce max file limit on patch content", () => { | ||||||||||
| // Create a patch with more than MAX_FILES (100) files | ||||||||||
| const patchLines = []; | ||||||||||
| for (let i = 0; i < 101; i++) { | ||||||||||
| patchLines.push(`diff --git a/file${i}.txt b/file${i}.txt`); | ||||||||||
| patchLines.push("index 1234567..abcdefg 100644"); | ||||||||||
| patchLines.push("--- a/file${i}.txt"); | ||||||||||
| patchLines.push("+++ b/file${i}.txt"); | ||||||||||
|
Comment on lines
+68
to
+71
|
||||||||||
| patchLines.push("@@ -1,1 +1,1 @@"); | ||||||||||
| patchLines.push("-old content"); | ||||||||||
| patchLines.push("+new content"); | ||||||||||
| } | ||||||||||
| const patchContent = patchLines.join("\n"); | ||||||||||
|
|
||||||||||
| // Import the enforcement function | ||||||||||
| const { enforcePullRequestLimits } = require("./create_pull_request.cjs"); | ||||||||||
|
|
||||||||||
| // Should throw E003 error | ||||||||||
| expect(() => enforcePullRequestLimits(patchContent)).toThrow("E003"); | ||||||||||
|
Comment on lines
+81
to
+82
|
||||||||||
| // Should throw E003 error | |
| expect(() => enforcePullRequestLimits(patchContent)).toThrow("E003"); | |
| // Should throw E002 error | |
| expect(() => enforcePullRequestLimits(patchContent)).toThrow("E002"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good practice: importing repo helpers for cross-repository validation.