-
Notifications
You must be signed in to change notification settings - Fork 430
safe-outputs: validate extracted base branch with git check-ref-format refs/heads/<name>
#40001
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 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,10 @@ | |
| /// <reference types="@actions/github-script" /> | ||
|
|
||
| const fs = require("fs"); | ||
| const { spawnSync } = require("child_process"); | ||
|
|
||
| const AGENT_OUTPUT_PATH = "/tmp/gh-aw/agent_output.json"; | ||
| const SAFE_BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.-]+$/; | ||
| const MAX_BRANCH_NAME_LENGTH = 255; | ||
|
|
||
| /** | ||
| * @param {string} itemRepo | ||
|
|
@@ -49,9 +50,22 @@ function extractBaseBranchFromAgentOutput(opts = {}) { | |
| async function main() { | ||
| const baseBranch = extractBaseBranchFromAgentOutput(); | ||
| if (!baseBranch) return; | ||
| if (!SAFE_BRANCH_NAME_REGEX.test(baseBranch) || baseBranch.length > 255) return; | ||
| if (!isValidBaseBranchName(baseBranch)) return; | ||
| core.setOutput("base-branch", baseBranch); | ||
| core.info(`Extracted base branch from safe output: ${baseBranch}`); | ||
| } | ||
|
|
||
| module.exports = { extractBaseBranchFromAgentOutput, isSameWorkflowRepo, main }; | ||
| /** | ||
| * @param {string} branchName | ||
| * @returns {boolean} | ||
| */ | ||
| function isValidBaseBranchName(branchName) { | ||
| if (!branchName || branchName.length > MAX_BRANCH_NAME_LENGTH) { | ||
| return false; | ||
| } | ||
|
|
||
| const result = spawnSync("git", ["check-ref-format", "--branch", branchName], { stdio: "ignore" }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Suggested fix
The old regex Use the full const result = spawnSync("git", ["check-ref-format", `refs/heads/${branchName}`], { stdio: "ignore" });This still accepts
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in the latest commit. Switched to |
||
| return !result.error && result.status === 0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] When 💡 Suggested comment// Fail-closed: if git is unavailable or the ref is invalid, reject the branch.
// A missing git binary sets result.error (ENOENT); a timeout sets it (ETIMEDOUT).
return !result.error && result.status === 0;The ADR already documents the conservative-rejection intent, but a one-liner here saves a future reader from needing to cross-reference it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an inline comment in the latest commit explaining ENOENT (missing git) and ETIMEDOUT (timeout) both set |
||
| } | ||
|
|
||
| module.exports = { extractBaseBranchFromAgentOutput, isSameWorkflowRepo, isValidBaseBranchName, main }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| import { describe, it, expect } from "vitest"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { extractBaseBranchFromAgentOutput, isSameWorkflowRepo } from "./extract_base_branch_from_agent_output.cjs"; | ||
| import { extractBaseBranchFromAgentOutput, isSameWorkflowRepo, isValidBaseBranchName } from "./extract_base_branch_from_agent_output.cjs"; | ||
|
|
||
| describe("extract_base_branch_from_agent_output", () => { | ||
| it("matches fully-qualified repos", () => { | ||
|
|
@@ -70,4 +70,16 @@ describe("extract_base_branch_from_agent_output", () => { | |
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| it("accepts valid git branch names used in safe outputs", () => { | ||
| expect(isValidBaseBranchName("feature/x")).toBe(true); | ||
| expect(isValidBaseBranchName("release/v1.2+hotfix")).toBe(true); | ||
| }); | ||
|
|
||
| it("rejects invalid git branch names even if they look regex-safe", () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing 💡 Suggested fixAdd to the "rejects invalid" block: expect(isValidBaseBranchName("@{-1}")).toBe(false);
expect(isValidBaseBranchName("@{-2}")).toBe(false);With the current implementation these assertions will fail (confirming the bug —
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in the latest commit — |
||
| expect(isValidBaseBranchName("foo..bar")).toBe(false); | ||
| expect(isValidBaseBranchName("main.lock")).toBe(false); | ||
| expect(isValidBaseBranchName(".foo")).toBe(false); | ||
| expect(isValidBaseBranchName("foo/.bar")).toBe(false); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The 💡 Suggested testit('enforces the 255-character length limit', () => {
const atLimit = 'a'.repeat(255);
const overLimit = 'a'.repeat(256);
expect(isValidBaseBranchName(atLimit)).toBe(true);
expect(isValidBaseBranchName(overLimit)).toBe(false);
});Without a boundary test, a future refactor that changes or removes the guard goes undetected by the suite.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a boundary test in the latest commit: 255-char name accepted, 256-char name rejected. |
||
| }); | ||
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.
[/diagnose]
spawnSynchas notimeoutoption — ifgitstalls (degraded runner, network-mounted git install), validation will block the step indefinitely.💡 Suggested fix
Add a
timeout(e.g. 5 seconds) so the function stays bounded:With
timeoutset, a hung subprocess is killed andresult.errorwill be set (ETIMEDOUT), so the existing!result.errorguard safely returnsfalse.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.
Added
timeout: 5000to thespawnSynccall in the latest commit.