Skip to content

Add explicit strict-mode policy for pull_request_target checkouts - #66649

Merged
pelikhan merged 8 commits into
mainfrom
copilot/support-strict-mode-policy
Oct 7, 2026
Merged

pelikhan merged 8 commits into
mainfrom
copilot/support-strict-mode-policy

Conversation

Copilot AI commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Strict mode rejects fixed external checkouts under pull_request_target and warns unconditionally, even when checkout is disabled or restricted to trusted base code. This adds a scoped opt-in for direct merged-PR workflows without disabling strict validation.

  • Checkout policy: pull-request-target.allowed-checkouts permits exact, case-sensitive literal repository/ref pairs. Every checkout must satisfy the allowlist or existing base-checkout rules; expressions, wildcards, omitted external refs, and PR refs remain rejected.
  • Warning acknowledgment: acknowledge-risk: true suppresses only the dangerous-trigger warning. Checkout validation and unrelated diagnostics remain intact.
  • Exact ref preservation: Configured checkout refs retain their string value in generated YAML, preventing literals such as 0x10 from becoming 16.
  • Compiler regressions and reference docs: Cover accepted/rejected configurations, warning isolation, credential wiring, and literal-ref preservation.
on:
  pull_request_target:
    types: [closed]
    branches: [main]
if: github.event.pull_request.merged == true

pull-request-target:
  acknowledge-risk: true
  allowed-checkouts:
    - repository: dotnet/AspNetCore.Docs
      ref: main

checkout:
  - repository: dotnet/AspNetCore.Docs
    ref: main
    path: .
    current: true

The policy records maintainer trust; authentication and safe-output publication remain separately configured. Defaults are unchanged when the policy is omitted.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Support explicit strict-mode policy for pull_request_target workflows Add explicit strict-mode policy for pull_request_target checkouts Oct 7, 2026
Copilot AI requested a review from pelikhan October 7, 2026 18:43
@pelikhan

pelikhan commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

@copilot

  • move fields under on.pull-request-target . Not a new top level field.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • move fields under on.pull-request-target . Not a new top level field.

Moved both options under on.pull_request_target across the schema, compiler validation, docs, and tests. Compiler-only options are excluded from generated event YAML. Commit: e8e3f449.

@pelikhan
pelikhan marked this pull request as ready for review October 7, 2026 19:20
Copilot AI balanced review requested due to automatic review settings October 7, 2026 19:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Checkout fetch refs bypass the new trust validation and can expose a privileged job to PR-controlled shell injection.

2 open findings
What changed in this PR

Adds an explicit strict-mode policy for trusted pull_request_target checkouts while retaining checkout validation.

Changes:

  • Adds checkout allowlisting and warning acknowledgment.
  • Preserves checkout refs as YAML strings.
  • Adds regression tests and reference documentation.
File Description
pkg/​workflow/​pull_request_target_validation.go Implements the new trust policy.
pkg/​workflow/​pull_request_target_validation_test.go Tests policy boundaries and ref preservation.
pkg/​workflow/​frontmatter_on_section_cleanup.go Removes compiler-only policy fields from output.
pkg/​workflow/​checkout_step_generator.go Uses safe ref rendering.
pkg/​workflow/​checkout_input_render.go Quotes YAML-sensitive refs.
pkg/​parser/​schemas/​main_workflow_schema.json Defines policy schema.
docs/​src/​content/​docs/​reference/​frontmatter.md Links strict-mode policy guidance.
docs/​src/​content/​docs/​reference/​checkout.md Documents trusted checkout configuration.

🧠 Review effort: Balanced

}
for _, cfg := range configs {
if !isTrustedPullRequestTargetCheckout(cfg) {
if !isTrustedPullRequestTargetCheckout(cfg) && !isAllowedPullRequestTargetCheckout(cfg, allowedCheckouts) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusted-base and allowlisted checkouts now fail closed when fetch is non-empty. Regression tests cover PR refs, PR-head expressions, and wildcard fetches. Commit: 79c1b9d7.

Comment on lines +1734 to +1739
"acknowledge-risk": {
"type": "boolean",
"default": false,
"description": "Acknowledge elevated permissions and secret access to suppress only the strict-mode pull_request_target trigger warning. Checkout validation remains enforced."
},
"allowed-checkouts": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository docs and copyable workflow example use on.pull_request_target. The PR body still has the earlier top-level example, and the available tools in this session do not expose PR-description editing; a maintainer will need to update that body.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #66649

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One simplification opportunity found.

net: -8 lines possible.

Generated by ✂️ Ponytail Reviewer for #66649 · codex · gpt56 · 12.8 AIC · ⌖ 6.34 AIC · ⊞ 13.4K
Comment /ponytail to run again

return token != ""
}

func writeCheckoutRef(sb *strings.Builder, ref string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L50: shrink: YAML unmarshal-and-compare solely to decide quoting. Always use strconv.Quote(ref); checkout refs are strings and that preserves every literal while removing the parser branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeCheckoutRef now always uses strconv.Quote(ref); the YAML unmarshal-and-compare branch is removed. Commit: 79c1b9d7.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required (draft added)

Gate status: ⛔ Blocking until the ADR is reviewed and accepted.

Why this PR was gated

What I did

I generated a draft ADR from the PR evidence and committed it to this branch:

  • docs/adr/66649-explicit-trusted-checkout-policy-for-pull-request-target.md

Decision captured: opt-in on.pull_request_target.allowed-checkouts (exact literal repository/ref pairs) plus acknowledge-risk to suppress only the strict-mode dangerous-trigger warning, with fail-closed validation and unchanged defaults.

Alternatives recorded: strict: false escape hatch, implicit trust of any non-expression literal external checkout, and a single global "unsafe" boolean.

Evidence used: pkg/workflow/pull_request_target_validation.go (+53/-14), pkg/parser/schemas/main_workflow_schema.json (+28), pkg/workflow/checkout_input_render.go, pkg/workflow/frontmatter_on_section_cleanup.go, docs/src/content/docs/reference/checkout.md, and issue #66564.

Your next action

Review the draft ADR, correct anything I inferred wrongly (notably the Deciders line marked [TODO: verify] and the security trade-offs in Consequences), then change Status from Draft to Accepted.

Note

The draft is my reading of the diff, not an approval of the security policy it describes. The negative consequences section intentionally states that this introduces a documented bypass of pull_request_target strict validation — please confirm that framing is accurate before accepting.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · opus50 · 45.3 AIC · ⌖ 50.4 AIC · ⊞ 1.7K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design and /grill-with-docs — requesting changes on a security-relevant gap in the new checkout allowlist.

📋 Key Themes & Highlights

Key Themes

  • Security gap: isAllowedPullRequestTargetCheckout / isTrustedPullRequestTargetCheckout validate repository and ref but never inspect cfg.Fetch. A checkout that passes the new allowlist (or the existing trusted-base check) can still declare an untrusted fetch ref (e.g. the PR head), and generateFetchStepLines will fetch it with the checkout's resolved credentials — this undermines the core security guarantee the PR is adding. See inline comment on pull_request_target_validation.go.
  • Doc/schema mismatch: the PR description's YAML example uses a top-level pull-request-target: key, but the schema and the new "top-level policy is rejected" test both require nesting under on.pull_request_target. The example should be corrected to avoid confusing adopters. See inline comment on main_workflow_schema.json.
  • Both of these align with and sharpen pre-existing review comments from Copilot's bot review — the fetch-bypass issue in particular should block merge since it's a silent security hole in a PR whose entire purpose is tightening pull_request_target checkout security.

Positive Highlights

  • ✅ Literal-pattern validation for repository/ref (rejecting expressions, wildcards, and refs/pull/...) is careful and well fail-closed, with strict.HasPrefix checks layered on top of regex for defense in depth.
  • ✅ Extensive table-driven regression tests cover most adversarial policy shapes (wildcard repo/ref, composite expressions, PR merge refs, empty ref, unknown fields, non-boolean acknowledgment).
  • ✅ writeCheckoutRef fix for literal-ref YAML scalar ambiguity (e.g. 0x10 becoming 16) is a nice, narrowly-scoped correctness fix with its own test coverage across multiple tricky literals.
  • ✅ Doc updates in checkout.md clearly explain the new policy's scope and limits, including an explicit caution that this records maintainer trust rather than proof of safety.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 57.6 AIC · ⌖ 14.7 AIC · ⊞ 10.1K
Comment /matt to run again

}
for _, cfg := range configs {
if !isTrustedPullRequestTargetCheckout(cfg) {
if !isTrustedPullRequestTargetCheckout(cfg) && !isAllowedPullRequestTargetCheckout(cfg, allowedCheckouts) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] isAllowedPullRequestTargetCheckout validates cfg.Repository and cfg.Ref but never checks cfg.Fetch. An entry that is otherwise allowlisted (or trusted-base) can still set fetch: ["${{ github.event.pull_request.head.ref }}"], and generateFetchStepLines will emit an authenticated git fetch of that untrusted ref with the checkout's resolved credentials — defeating the allowlist's safety guarantee entirely.

💡 Suggested fix

Treat a non-empty cfg.Fetch the same as an untrusted repository/ref: either reject any allowlisted/trusted checkout that sets fetch to a non-literal value, or require each fetch entry to pass the same literal-pattern checks (pullRequestTargetLiteralRefPattern, no refs/pull/ prefix) as cfg.Ref. Add a regression test with checkout.fetch set to a PR-head expression paired with an otherwise-allowlisted checkout to lock in the fix — this is exactly the kind of case /diagnosing-bugs regression-test discipline would catch before merge.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusted-base and allowlisted checkouts now fail closed when fetch is non-empty. Regression tests cover PR-head expressions and PR refs. Commit: 79c1b9d7.

"default": false,
"description": "Acknowledge elevated permissions and secret access to suppress only the strict-mode pull_request_target trigger warning. Checkout validation remains enforced."
},
"allowed-checkouts": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The PR description's copyable example configures the policy under a top-level pull-request-target: key, but the schema (and the new test "top-level policy is rejected") place these options under on.pull_request_target. A maintainer copy-pasting the PR description's YAML will get a hard schema-rejection instead of the intended behavior.

💡 Suggested fix

Align the PR description/example with the actual schema location (on.pull_request_target.acknowledge-risk / on.pull_request_target.allowed-checkouts), matching the docs in checkout.md which already use the correct nesting. This is a documentation-vocabulary mismatch the /grill-with-docs lens is meant to catch — the same domain concept (pull_request_target policy) is named inconsistently between the PR body and the schema/tests.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository docs and copyable workflow example use on.pull_request_target. The PR body still has the earlier top-level example, and the available tools in this session do not expose PR-description editing; a maintainer will need to update that body.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Impeccable mode applied: harden + audit (change type: refactor_cleanup/security-policy addition touching compiler validation, schema, and docs).

Two existing review comments from Copilot already flag the two actionable issues I found, so I'm not duplicating them inline:

  1. pkg/workflow/pull_request_target_validation.go:186 (blocking) — isAllowedPullRequestTargetCheckout/isTrustedPullRequestTargetCheckout never validate cfg.Fetch. A checkout matching the new allowlist (or even the trusted-base case) can still carry fetch: ["refs/pull/*/head"] or an expression-based ref, and generateFetchStepLines will emit a git fetch step pulling untrusted refs into the "trusted" checkout — reopening the pwn-request vector this PR is meant to close, while strict validation reports it as safe. I confirmed Fetch []string is a real CheckoutConfig field (pkg/workflow/checkout_manager.go:97-110) consumed unguarded by generateFetchStepLines (pkg/workflow/checkout_step_generator.go:477+).
  2. pkg/parser/schemas/main_workflow_schema.json:1739 (non-blocking but should fix before merge) — the PR description's copy-pasteable YAML example nests the policy under a top-level pull-request-target: block, but the actual schema/compiler implementation (and the PR's own new test "top-level policy is rejected") require on.pull_request_target.acknowledge-risk / on.pull_request_target.allowed-checkouts. The example in the PR body will fail to compile as written.

No new inline comments added to avoid duplicating the bot's existing findings. Recommend addressing #1 before merge since it undermines the security guarantee the PR adds.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · copilot · sonnet50 · 61.8 AIC · ⌖ 13.1 AIC · ⊞ 8.1K

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Comment Memory

Peek at saved memory (pr-code-quality-reviewer)
reviewed_at: 2026-10-07T19:42:34Z
review_event: REQUEST_CHANGES
top_themes:
  - pull_request_target allowlist ignores checkout.fetch and still permits privileged PR-ref imports
files_reviewed:
  - docs/src/content/docs/reference/checkout.md
  - docs/src/content/docs/reference/frontmatter.md
  - pkg/parser/schemas/main_workflow_schema.json
  - pkg/workflow/checkout_input_render.go
  - pkg/workflow/checkout_step_generator.go
  - pkg/workflow/frontmatter_on_section_cleanup.go
  - pkg/workflow/pull_request_target_validation.go
  - pkg/workflow/pull_request_target_validation_test.go
comment_count: 1

Note

This comment is managed by comment memory.

Expand the saved memory block to view or edit the persistent context for this thread.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 67 AIC · ⌖ 5.57 AIC · ⊞ 19.6K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The new pull_request_target allowlist still leaves a privileged code-loading path open: validation only blesses the initial checkout pair, but the generated fetch: step can still import untrusted PR refs into the same workspace. That means strict mode can now approve workflows that are still one git checkout away from a classic pwn-request shape.

Blocking theme
  • allowed-checkouts currently ignores checkout.fetch, even though generateFetchStepLines runs in the same job and can fetch refs/pull/*/head or wildcard branches after the “trusted” checkout is complete.
  • The new docs/tests currently describe the policy as if every effective checkout surface is covered, but there is no regression proving that fetch: remains forbidden under pull_request_target.

I would be comfortable with this once the trust decision treats fetch: as part of the checkout surface and the new policy tests include explicit rejection cases for PR-head and wildcard fetch patterns.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 67 AIC · ⌖ 5.57 AIC · ⊞ 19.6K
Comment /review to run again

{name: "acknowledgment alone does not allow external checkout", policy: acknowledged, checkout: externalCheckout, errorContains: insecureCheckout},
{name: "allowlist alone retains warning", policy: allowlist, checkout: externalCheckout, warnings: 1},
{name: "false acknowledgment retains warning", policy: " acknowledge-risk: false\n" + allowlist, checkout: externalCheckout, warnings: 1},
{name: "acknowledged literal external checkout", policy: acknowledged + allowlist, checkout: externalCheckout},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test matrix still misses the core pull_request_target bypass: an allowlisted checkout can add fetch: (for example refs/pulls/open/*) and the generated follow-up fetch step will pull untrusted PR refs into the same privileged workspace.

💡 Why this blocks the change

hasOnlyTrustedPullRequestTargetCheckouts now treats the checkout as safe as soon as the initial repository/ref pair matches, but generateFetchStepLines can immediately widen that workspace again by fetching PR heads or arbitrary branches. That means strict mode will approve a workflow that still makes untrusted code available to later git checkout or script steps under pull_request_target credentials.

Please add a regression case that keeps allowed-checkouts enabled but sets checkout.fetch to a PR ref or wildcard pattern, and make validation reject it. Until fetch: is part of the trust decision, the new allowlist is not actually enforcing “trusted checkouts only”.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added strict-mode regression cases for allowlisted PR-ref/expression fetches and a trusted-base wildcard fetch; these no longer pass trust validation. Commit: 79c1b9d7.

@pelikhan

pelikhan commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran the PR finisher. Commit 79c1b9d7 rejects checkout.fetch for trusted pull_request_target checkouts and adds regression coverage; the local final gate passed. The PR-body example still needs a maintainer edit, review threads remain unresolved because this session has no resolution tool, and CI must be re-triggered by a maintainer after this push.

@pelikhan

pelikhan commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

@copilot update compiler threat spec and TLA+ model

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update compiler threat spec and TLA+ model

Updated CTR-008 and added a bounded TLA+ model for acknowledgment scope, checkout trust, and fetch rejection. Commit: 53d82776.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot address the following outstanding work in one pass:

  1. Update this branch with the latest main using make merge-main, resolving any conflicts and preserving the intended changes.
  2. Fix failing check impacted-go-tests (FAILURE): https://github.com/github/gh-aw/actions/runs/37682308676/job/113009226257.

Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI.

Sous-chef head: 53d8277
Sous-chef work: 42e5d572d9d75b7168fe7d0aeda0717cc07e65ed240f6d299e6fec661214f1ac
Sous-chef state: 2b5d36e957041805ff06be731ae2d34f80f8fa50d1d9360f3cb5ebd5183738f2

Generated by 👨‍🍳 PR Sous Chef · pi · haiku45 · 4.45 AIC · ⌖ 7.88 AIC · ⊞ 1K · ◷
Comment /souschef to run again

Copilot AI and others added 2 commits October 7, 2026 21:16
…t-mode-policy

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address the following outstanding work in one pass:

  1. Update this branch with the latest main using make merge-main, resolving any conflicts and preserving the intended changes....

Merged latest main in 07dcc81 and fixed the impacted tests in a4a8674 by updating quoted-ref assertions and regenerating WASM goldens. go test ./pkg/workflow -count=1, make build, and make lint passed. The final gate’s tests passed but it remains blocked by custom-linter findings in unchanged code; review threads already have replies, but this session lacks GitHub CLI mutation authorization to resolve them. CI was not triggered and needs a maintainer re-run.

Copilot AI requested a review from gh-aw-bot October 7, 2026 21:41
@pelikhan
pelikhan merged commit 303b402 into main Oct 7, 2026
36 checks passed
@pelikhan
pelikhan deleted the copilot/support-strict-mode-policy branch October 7, 2026 22:38
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.91.6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support an explicit strict-mode policy for pull_request_target workflows with fixed external checkouts

4 participants