From 5553049b46b752c7bfd419dc41baaf8d15598483 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 04:30:56 +0000
Subject: [PATCH 1/5] Initial plan
From 687c879a9629285c2b47487e45e21830645b9974 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 04:43:42 +0000
Subject: [PATCH 2/5] fix(SEC-005): add validateTargetRepo allowlist checks to
cross-repo handlers
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/dynamic_checkout.cjs | 15 ++++++++++++++-
actions/setup/js/extra_empty_commit.cjs | 16 +++++++++++++++-
actions/setup/js/find_repo_checkout.cjs | 15 ++++++++++++++-
actions/setup/js/get_base_branch.cjs | 17 +++++++++++++++++
actions/setup/js/safe_outputs_tools_loader.cjs | 10 ++++++++++
5 files changed, 70 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/dynamic_checkout.cjs b/actions/setup/js/dynamic_checkout.cjs
index 8f420f2aaa6..12d1dc57b5c 100644
--- a/actions/setup/js/dynamic_checkout.cjs
+++ b/actions/setup/js/dynamic_checkout.cjs
@@ -1,6 +1,9 @@
// @ts-check
///
+const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs");
+const { ERR_VALIDATION } = require("./error_codes.cjs");
+
/**
* Dynamic repository checkout utilities for multi-repo scenarios
* Enables switching between different repositories during handler execution
@@ -60,10 +63,20 @@ async function checkoutRepo(repoSlug, token, options = {}) {
if (parts.length !== 2 || !parts[0] || !parts[1]) {
return {
success: false,
- error: `Invalid repository slug: ${repoSlug}. Expected format: owner/repo`,
+ error: `${ERR_VALIDATION}: Invalid repository slug: ${repoSlug}. Expected format: owner/repo`,
};
}
+ // Validate target repo against configured allowlist before any git operations
+ const allowedRepos = parseAllowedRepos(options.allowedRepos);
+ if (allowedRepos.size > 0) {
+ const defaultRepo = getDefaultTargetRepo();
+ const validation = validateTargetRepo(repoSlug, defaultRepo, allowedRepos);
+ if (!validation.valid) {
+ return { success: false, error: `${ERR_VALIDATION}: ${validation.error}` };
+ }
+ }
+
const [owner, repo] = parts;
core.info(`Switching checkout to repository: ${repoSlug}`);
diff --git a/actions/setup/js/extra_empty_commit.cjs b/actions/setup/js/extra_empty_commit.cjs
index b58b0a3ac91..4a7a2d64027 100644
--- a/actions/setup/js/extra_empty_commit.cjs
+++ b/actions/setup/js/extra_empty_commit.cjs
@@ -1,6 +1,8 @@
// @ts-check
///
+const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs");
+
/**
* @fileoverview Extra Empty Commit Helper
*
@@ -49,7 +51,7 @@ function isCrossRepoTarget(repoOwner, repoName) {
* modifications on multi-commit branches and reducing loop risk.
* @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>}
*/
-async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage, newCommitCount }) {
+async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage, newCommitCount, allowedRepos: allowedReposInput }) {
const token = process.env.GH_AW_CI_TRIGGER_TOKEN;
if (!token || !token.trim()) {
@@ -57,6 +59,18 @@ async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMes
return { success: true, skipped: true };
}
+ // Validate target repository against allowlist before any git operations
+ const allowedRepos = parseAllowedRepos(allowedReposInput);
+ if (allowedRepos.size > 0) {
+ const targetRepo = `${repoOwner}/${repoName}`;
+ const defaultRepo = getDefaultTargetRepo();
+ const validation = validateTargetRepo(targetRepo, defaultRepo, allowedRepos);
+ if (!validation.valid) {
+ core.warning(`ERR_VALIDATION: ${validation.error}`);
+ return { success: false, error: validation.error };
+ }
+ }
+
// Cross-repo guard: never push an extra empty commit to a different repository.
// A token is needed to create the PR and that will trigger events anyway.
if (isCrossRepoTarget(repoOwner, repoName)) {
diff --git a/actions/setup/js/find_repo_checkout.cjs b/actions/setup/js/find_repo_checkout.cjs
index 38a556b4fdc..2421b303d5c 100644
--- a/actions/setup/js/find_repo_checkout.cjs
+++ b/actions/setup/js/find_repo_checkout.cjs
@@ -3,6 +3,7 @@
const fs = require("fs");
const path = require("path");
const { execGitSync } = require("./git_helpers.cjs");
+const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs");
/**
* Debug logging helper - logs to stderr when DEBUG env var matches
@@ -129,9 +130,11 @@ function getRemoteOriginUrl(repoPath) {
*
* @param {string} repoSlug - The repository slug to find (owner/repo format)
* @param {string} [workspaceRoot] - The workspace root to search from
+ * @param {Object} [options] - Additional options
+ * @param {string[]|string} [options.allowedRepos] - Allowed repository patterns for validation
* @returns {Object} Result with success status and path or error
*/
-function findRepoCheckout(repoSlug, workspaceRoot) {
+function findRepoCheckout(repoSlug, workspaceRoot, options = {}) {
const ws = workspaceRoot || process.env.GITHUB_WORKSPACE || process.cwd();
const targetSlug = normalizeRepoSlug(repoSlug);
@@ -144,6 +147,16 @@ function findRepoCheckout(repoSlug, workspaceRoot) {
};
}
+ // Validate target repo against configured allowlist before searching
+ const allowedRepos = parseAllowedRepos(options.allowedRepos);
+ if (allowedRepos.size > 0) {
+ const defaultRepo = getDefaultTargetRepo();
+ const validation = validateTargetRepo(targetSlug, defaultRepo, allowedRepos);
+ if (!validation.valid) {
+ return { success: false, error: validation.error };
+ }
+ }
+
// Find all git directories in the workspace
const gitDirs = findGitDirectories(ws);
debugLog(`Found ${gitDirs.length} git directories: ${gitDirs.join(", ")}`);
diff --git a/actions/setup/js/get_base_branch.cjs b/actions/setup/js/get_base_branch.cjs
index 1c5e220d0d2..ac2a692c2fd 100644
--- a/actions/setup/js/get_base_branch.cjs
+++ b/actions/setup/js/get_base_branch.cjs
@@ -1,6 +1,8 @@
// @ts-check
///
+const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs");
+
/**
* Get the base branch name, resolving dynamically based on event context.
*
@@ -40,6 +42,21 @@ async function getBaseBranch(targetRepo = null) {
if (typeof github !== "undefined") {
const repoOwner = targetRepo?.owner ?? context.repo.owner;
const repoName = targetRepo?.repo ?? context.repo.repo;
+
+ // Validate target repo against allowlist before any API calls
+ const targetRepoSlug = `${repoOwner}/${repoName}`;
+ const allowedRepos = parseAllowedRepos(process.env.GH_AW_ALLOWED_REPOS);
+ if (allowedRepos.size > 0) {
+ const defaultRepo = getDefaultTargetRepo();
+ const validation = validateTargetRepo(targetRepoSlug, defaultRepo, allowedRepos);
+ if (!validation.valid) {
+ if (typeof core !== "undefined") {
+ core.warning(`ERR_VALIDATION: ${validation.error}`);
+ }
+ return process.env.DEFAULT_BRANCH || "main";
+ }
+ }
+
const { data: pr } = await github.rest.pulls.get({
owner: repoOwner,
repo: repoName,
diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs
index b800d459519..c1b9d3d313a 100644
--- a/actions/setup/js/safe_outputs_tools_loader.cjs
+++ b/actions/setup/js/safe_outputs_tools_loader.cjs
@@ -1,6 +1,7 @@
// @ts-check
const { getErrorMessage } = require("./error_helpers.cjs");
+const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs");
const fs = require("fs");
@@ -100,6 +101,15 @@ function registerPredefinedTools(server, tools, config, registerTool, normalizeT
if (tool.name === "create_pull_request" && config.create_pull_request) {
const targetRepo = config.create_pull_request["target-repo"];
if (targetRepo) {
+ // Validate the configured target-repo against the allowed-repos list
+ const allowedRepos = parseAllowedRepos(config.create_pull_request.allowed_repos);
+ if (allowedRepos.size > 0) {
+ const defaultRepo = getDefaultTargetRepo(config.create_pull_request);
+ const validation = validateTargetRepo(targetRepo, defaultRepo, allowedRepos);
+ if (!validation.valid) {
+ server.debug(`WARNING: SEC-005: ${validation.error}`);
+ }
+ }
toolToRegister = JSON.parse(JSON.stringify(tool));
toolToRegister.description += ` Note: This workflow is configured to create pull requests in '${targetRepo}'. You do not need to specify the repo parameter.`;
if (toolToRegister.inputSchema && toolToRegister.inputSchema.properties && toolToRegister.inputSchema.properties.repo) {
From c678bb529988bf152d96f89f4d7e23a0e21d5220 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 3 Mar 2026 04:49:00 +0000
Subject: [PATCH 3/5] Add changeset [skip-ci]
---
.changeset/patch-validate-cross-repo-allowlist.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/patch-validate-cross-repo-allowlist.md
diff --git a/.changeset/patch-validate-cross-repo-allowlist.md b/.changeset/patch-validate-cross-repo-allowlist.md
new file mode 100644
index 00000000000..656f06d4b66
--- /dev/null
+++ b/.changeset/patch-validate-cross-repo-allowlist.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": patch
+---
+
+Add allowlist validation for the cross-repo helpers so `targetRepo` parameters and GH_AW_ALLOWED_REPOS checks now guard git and API work before any operations.
From d628e24222ff944b99bf0d4c28aad3d421870eac Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 3 Mar 2026 04:49:58 +0000
Subject: [PATCH 4/5] ci: trigger checks
From 8d0af6d607bb1a3c890cddf1e27b998cc8529fa0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 05:03:07 +0000
Subject: [PATCH 5/5] fix(SEC-005): fix TypeScript errors in JSDoc annotations
for allowedRepos params
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/dynamic_checkout.cjs | 1 +
actions/setup/js/extra_empty_commit.cjs | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/actions/setup/js/dynamic_checkout.cjs b/actions/setup/js/dynamic_checkout.cjs
index 12d1dc57b5c..f565bee902b 100644
--- a/actions/setup/js/dynamic_checkout.cjs
+++ b/actions/setup/js/dynamic_checkout.cjs
@@ -54,6 +54,7 @@ async function getCurrentCheckoutRepo() {
* @param {string} token - GitHub token for authentication
* @param {Object} options - Additional options
* @param {string} [options.baseBranch] - Base branch to checkout (defaults to 'main')
+ * @param {string[]|string} [options.allowedRepos] - Allowed repository patterns for allowlist validation
* @returns {Promise