Repository: github/gh-aw (2026-04-19T23:34:14Z)

Issue:
[Safe Outputs Conformance] SEC-005: invocation_context_helpers.cjs supports target-repo but lacks allowlist check
### Conformance Check Failure

**Check ID**: SEC-005
**Severity**: HIGH
**Category**: Security

### Problem Description

`actions/setup/js/invocation_context_helpers.cjs` reads `target_repo` / `targetRepo` from workflow inputs and uses it to derive the event repository context. The Safe Outputs specification requires that any cross-repository operation validate the target against an explicit allowlist to prevent unauthorized writes to arbitrary repositories.

The conformance check looks for `allowed.*[Rr]epos`, `validateTargetRepo`, or `checkAllowedRepo` — none of which are present in the file.

### Affected Components

- **File**: `actions/setup/js/invocation_context_helpers.cjs` (line 154)

<details>
<summary>🔍 Current vs Expected Behavior</summary>

### Current Behavior

The handler parses `target_repo` / `targetRepo` from inputs and assigns it to `eventRepo` without verifying whether the supplied repository slug is on an allowlist:

```js
eventRepo = eventRepo || parseRepoSlug(inputs.event_repo) || parseRepoSlug(inputs.eventRepo) || parseRepoSlug(inputs.target_repo) || parseRepoSlug(inputs.targetRepo);
```

### Expected Behavior

Before using a caller-supplied `target_repo` value, the handler should validate it against an allowlist (e.g. `ALLOWED_REPOS` env var, a hardcoded list, or a call to `validateTargetRepo(repo)` / `checkAllowedRepo(repo)`). If the repo is not on the allowlist, the handler should throw with a standardized error code.

</details>

### Remediation Steps

This task can be assigned to a Copilot coding agent with the following steps:

1. Determine the appropriate allowlist source (environment variable, config file, or hardcoded list of permitted org/repo slugs).
2. Add a `validateTargetRepo(repo)` or `checkAllowedRepo(repo)` helper that throws an error (using a standardized E-code) when the repo is not on the allowlist.
3. Call the helper immediately after `eventRepo` is resolved from `target_repo` / `targetRepo` inputs.
4. Add a `@safe-outputs-exempt SEC-005` annotation only if this file is genuinely exempt (e.g. it never performs cross-repo writes) — otherwise apply the full check.
5. Add or update a unit test covering the rejection case.

### Verification

After remediation, verify the fix by running:

```bash
bash scripts/check-safe-outputs-conformance.sh
```

Check SEC-005 should pass without errors.

### References

- Safe Outputs Specification: `docs/src/content/docs/reference/safe-outputs-specification.md`
- Conformance Checker: `scripts/check-safe-outputs-conformance.sh`
- Run ID: [§24640126160](https://github.com/github/gh-aw/actions/runs/24640126160)
- Date: 2026-04-19


<!-- gh-aw-tracker-id: safe-outputs-conformance -->




> Generated by [Daily Safe Outputs Conformance Checker](https://github.com/github/gh-aw/actions/runs/24640126160/agentic_workflow) · ● 97K · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Aissue+%22gh-aw-workflow-call-id%3A+github%2Fgh-aw%2Fdaily-safe-outputs-conformance%22&type=issues)
> - [x] expires <!-- gh-aw-expires: 2026-04-20T22:02:58.341Z --> on Apr 20, 2026, 10:02 PM UTC

<!-- gh-aw-agentic-workflow: Daily Safe Outputs Conformance Checker, gh-aw-tracker-id: safe-outputs-conformance, engine: claude, model: auto, id: 24640126160, workflow_id: daily-safe-outputs-conformance, run: https://github.com/github/gh-aw/actions/runs/24640126160 -->

<!-- gh-aw-workflow-id: daily-safe-outputs-conformance -->
<!-- gh-aw-workflow-call-id: github/gh-aw/daily-safe-outputs-conformance -->


Discussion:

Patch:
diff --git a/actions/setup/js/invocation_context_helpers.cjs b/actions/setup/js/invocation_context_helpers.cjs
index ec97258803..238cc86f15 100644
--- a/actions/setup/js/invocation_context_helpers.cjs
+++ b/actions/setup/js/invocation_context_helpers.cjs
@@ -1,7 +1,8 @@
 // @ts-check
 /// <reference types="@actions/github-script" />
 
-const { parseRepoSlug: parseSharedRepoSlug } = require("./repo_helpers.cjs");
+const { parseRepoSlug: parseSharedRepoSlug, parseAllowedRepos, validateTargetRepo } = require("./repo_helpers.cjs");
+const { ERR_VALIDATION } = require("./error_codes.cjs");
 
 /**
  * @typedef {{ owner: string, repo: string }} RepoRef
@@ -106,6 +107,22 @@ function parseJSONPayload(value) {
   return null;
 }
 
+/**
+ * Validate workflow_dispatch target repository against allowlist configuration.
+ * Enforces SEC-005 by rejecting disallowed cross-repository target overrides.
+ * @param {RepoRef} workflowRepo
+ * @param {RepoRef} targetRepo
+ */
+function checkAllowedRepo(workflowRepo, targetRepo) {
+  const defaultRepo = `${workflowRepo.owner}/${workflowRepo.repo}`;
+  const targetRepoSlug = `${targetRepo.owner}/${targetRepo.repo}`;
+  const allowedRepos = parseAllowedRepos(process.env.GH_AW_ALLOWED_REPOS);
+  const validation = validateTargetRepo(targetRepoSlug, defaultRepo, allowedRepos);
+  if (!validation.valid) {
+    throw new Error(`${ERR_VALIDATION}: ${validation.error}`);
+  }
+}
+
 /**
  * Resolve workflow repo and effective event context across invocation styles:
  * - native events
@@ -145,13 +162,17 @@ function resolveInvocationContext(rawContext) {
     if (inputs && typeof inputs === "object") {
       const inputsEventName = typeof inputs.event_name === "string" ? inputs.event_name : typeof inputs.eventName === "string" ? inputs.eventName : "";
       const parsedPayload = parseJSONPayload(inputs.event_payload) || parseJSONPayload(inputs.eventPayload);
+      const targetRepo = parseRepoSlug(inputs.target_repo) || parseRepoSlug(inputs.targetRepo);
+      if (targetRepo) {
+        checkAllowedRepo(workflowRepo, targetRepo);
+      }
       if (inputsEventName) {
         eventName = inputsEventName;
       }
       if (parsedPayload) {
         eventPayload = parsedPayload;
       }
-      eventRepo = eventRepo || parseRepoSlug(inputs.event_repo) || parseRepoSlug(inputs.eventRepo) || parseRepoSlug(inputs.target_repo) || parseRepoSlug(inputs.targetRepo);
+      eventRepo = eventRepo || parseRepoSlug(inputs.event_repo) || parseRepoSlug(inputs.eventRepo) || targetRepo;
     }
   }
 


Test patch:
diff --git a/actions/setup/js/invocation_context_helpers.test.cjs b/actions/setup/js/invocation_context_helpers.test.cjs
index 5a3e1ba8e7..0b4de98126 100644
--- a/actions/setup/js/invocation_context_helpers.test.cjs
+++ b/actions/setup/js/invocation_context_helpers.test.cjs
@@ -67,4 +67,69 @@ describe("invocation_context_helpers", () => {
     expect(resolved.eventRepo).toEqual({ owner: "target-owner", repo: "target-repo" });
     expect(resolved.eventPayload.issue.number).toBe(777);
   });
+
+  it.each(["target_repo", "targetRepo"])("rejects workflow_dispatch %s when not in allowlist", targetRepoKey => {
+    const originalAllowedRepos = process.env.GH_AW_ALLOWED_REPOS;
+    try {
+      process.env.GH_AW_ALLOWED_REPOS = "allowed-owner/allowed-repo";
+
+      expect(() =>
+        resolveInvocationContext({
+          eventName: "workflow_dispatch",
+          repo: { owner: "side-owner", repo: "side-repo" },
+          payload: {
+            inputs: {
+              [targetRepoKey]: "target-owner/target-repo",
+            },
+          },
+        })
+      ).toThrow(/ERR_VALIDATION: Repository 'target-owner\/target-repo' is not in the allowed-repos list/);
+    } finally {
+      if (originalAllowedRepos === undefined) {
+        delete process.env.GH_AW_ALLOWED_REPOS;
+      } else {
+        process.env.GH_AW_ALLOWED_REPOS = originalAllowedRepos;
+      }
+    }
+  });
+
+  it("allows workflow_dispatch target_repo when it is in allowlist", () => {
+    const originalAllowedRepos = process.env.GH_AW_ALLOWED_REPOS;
+    try {
+      process.env.GH_AW_ALLOWED_REPOS = "target-owner/target-repo";
+
+      const resolved = resolveInvocationContext({
+        eventName: "workflow_dispatch",
+        repo: { owner: "side-owner", repo: "side-repo" },
+        payload: {
+          inputs: {
+            target_repo: "target-owner/target-repo",
+          },
+        },
+      });
+
+      expect(resolved.eventRepo).toEqual({ owner: "target-owner", repo: "target-repo" });
+    } finally {
+      if (originalAllowedRepos === undefined) {
+        delete process.env.GH_AW_ALLOWED_REPOS;
+      } else {
+        process.env.GH_AW_ALLOWED_REPOS = originalAllowedRepos;
+      }
+    }
+  });
+
+  it("allows workflow_dispatch without target_repo inputs", () => {
+    const resolved = resolveInvocationContext({
+      eventName: "workflow_dispatch",
+      repo: { owner: "side-owner", repo: "side-repo" },
+      payload: {
+        inputs: {
+          event_name: "issues",
+        },
+      },
+    });
+
+    expect(resolved.eventName).toBe("issues");
+    expect(resolved.eventRepo).toEqual({ owner: "side-owner", repo: "side-repo" });
+  });
 });