Skip to content

Add pull_request_target security validation (pwn request detection)#29433

Merged
pelikhan merged 3 commits intomainfrom
copilot/add-pull-request-target-check
May 1, 2026
Merged

Add pull_request_target security validation (pwn request detection)#29433
pelikhan merged 3 commits intomainfrom
copilot/add-pull-request-target-check

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

pull_request_target runs with full write permissions and secret access on the base branch, making it a critical attack surface ("pwn request") when combined with checkout of untrusted PR code.

Behavior

Mode checkout: false No checkout: false
non-strict ⚠ warning
strict ⚠ warning (dangerous trigger) ⚠ warning + 🚫 error

In strict mode, a warning is always emitted regardless of checkout state — even a checkout: false workflow still runs with elevated privileges.

Changes

  • pull_request_target_validation.go — new validatePullRequestTargetTrigger method on *Compiler:

    • Fast-path string check (same pattern as validateWorkflowRunBranches), then YAML parse to confirm the trigger key
    • Strict + no checkout: hard error citing the pwn-request attack vector
    • Strict + checkout disabled: warning that the trigger itself is inherently dangerous
    • Non-strict + no checkout: warning with remediation (checkout: false)
  • permissions_compiler_validator.go — calls the new validator as step 6 in validatePermissions, after validateWorkflowRunBranches

  • pull_request_target_validation_test.go — 7 unit test cases covering both modes, checkout: false on/off, and non-pull_request_target triggers that must not regress

  • pkg/cli/workflows/test-pull-request-target-checkout-false.md — integration workflow fixture: pull_request_target with shared-workflow imports and checkout: false

  • pkg/cli/workflows/test-pull-request-target-with-imports.md — integration workflow fixture: pull_request_target with shared-workflow imports, no checkout: false

  • compile_pull_request_target_integration_test.go — 4 integration sub-tests confirming the validation works end-to-end when shared workflow imports are present, covering non-strict (warning) and strict (error) modes

- Error in strict mode when checkout is not false (extremely insecure pwn request vector)
- Warning in non-strict mode when checkout is not false
- Warning always in strict mode that pull_request_target is a very dangerous trigger even with checkout: false
- 7 test cases covering all scenarios

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/a3b45cce-5f26-4c14-944f-be9230cce0f1

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

pelikhan commented May 1, 2026

@copilot make sure it works with imported shared agentic workflows, add integration workflows in pkg/cli/workflows

…rget validation

- Add test-pull-request-target-checkout-false.md fixture (PRT + imports + checkout:false)
- Add test-pull-request-target-with-imports.md fixture (PRT + imports, no checkout:false)
- Add compile_pull_request_target_integration_test.go with 4 integration sub-tests
  covering non-strict (warning) and strict (error) modes with imported shared workflows

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/14bec267-67b9-4244-9a72-e297378d1a51

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot make sure it works with imported shared agentic workflows, add integration workflows in pkg/cli/workflows

Done in cffaacf. Added two workflow fixtures that include shared imports:

  • pkg/cli/workflows/test-pull-request-target-checkout-false.md — PRT + imports: [shared/keep-it-short.md] + checkout: false
  • pkg/cli/workflows/test-pull-request-target-with-imports.md — PRT + imports: [shared/keep-it-short.md], no checkout: false

And pkg/cli/compile_pull_request_target_integration_test.go with 4 integration sub-tests covering both non-strict (warning only) and strict (error) modes, confirming the validation works correctly when shared workflow imports are present.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/actions/github-script/git/ref/tags/v9
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9 --jq [.object.sha, .object.type] | @tsv (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from pelikhan May 1, 2026 04:12
@pelikhan pelikhan marked this pull request as ready for review May 1, 2026 04:17
Copilot AI review requested due to automatic review settings May 1, 2026 04:17
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100

Excellent test quality

Metric Value
New/modified tests analyzed 3 test functions (9 sub-cases via table-driven tests)
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (100%)
Duplicate test clusters 0
Test inflation detected No (max ratio: 1.6:1)
🚨 Coding-guideline violations None

Test Classification Details

View All Test Classifications
Test File Classification Notes
TestPullRequestTargetValidation (7 sub-cases) pkg/workflow/pull_request_target_validation_test.go:14 ✅ Design Table-driven; covers checkout-enabled vs disabled, strict vs non-strict, non-target triggers, error message content, warning counts
TestPullRequestTargetCheckoutFalseWithImports (2 sub-cases) pkg/cli/compile_pull_request_target_integration_test.go:23 ✅ Design Integration test; compiles real workflow binary, asserts on emitted warnings and absence of insecure-checkout error
TestPullRequestTargetWithImportsNoCheckoutFalse (3 sub-cases) pkg/cli/compile_pull_request_target_integration_test.go:65 ✅ Design Integration test; verifies binary exits non-zero in strict mode and correct diagnostic text appears

Language Support

Tests analyzed:

  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All three test functions verify observable security behavior — the correct warning/error state, message content, and exit code under each trigger/mode combination. The table-driven unit test covers 7 distinct scenarios including happy paths, error paths, and boundary conditions (checkout enabled vs. disabled, strict vs. non-strict, non-target triggers). Both integration tests compile real workflow fixtures against the actual binary, giving strong end-to-end confidence.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References:

🧪 Test quality analysis by Test Quality Sentinel · ● 343.8K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All tests verify observable security behavior with good error-path and edge-case coverage.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new security validator to detect risky pull_request_target workflows (the “pwn request” footgun), emitting warnings in non-strict mode and escalating to hard errors in strict mode when PR code checkout is not explicitly disabled.

Changes:

  • Added validatePullRequestTargetTrigger to detect pull_request_target triggers and enforce checkout safety rules.
  • Hooked the new validator into the permissions validation pipeline.
  • Added unit + integration tests and workflow fixtures (including cases with shared-workflow imports).
Show a summary per file
File Description
pkg/workflow/pull_request_target_validation.go Implements pull_request_target trigger security validation (warn/error based on strictness + checkout state).
pkg/workflow/permissions_compiler_validator.go Invokes the new validator during permission validation.
pkg/workflow/pull_request_target_validation_test.go Adds unit tests for strict/non-strict behavior and checkout disabled/enabled variants.
pkg/cli/workflows/test-pull-request-target-checkout-false.md Adds an integration fixture for pull_request_target with checkout: false and imports.
pkg/cli/workflows/test-pull-request-target-with-imports.md Adds an integration fixture for pull_request_target with imports and checkout enabled.
pkg/cli/compile_pull_request_target_integration_test.go Adds end-to-end integration coverage for warnings/errors with imported workflows.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 4

Comment on lines +75 to +83
onData, hasOn := parsedData["on"]
if !hasOn {
return nil
}

onMap, isMap := onData.(map[string]any)
if !isMap {
return nil
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

validatePullRequestTargetTrigger only handles the case where the parsed on value is a map (i.e., on: { pull_request_target: ... }). Workflows can also legally declare events as a scalar (on: pull_request_target) or sequence (on: [pull_request_target]), which would make onData a string/slice and this validator would silently skip, missing the intended security diagnostic. Please extend the parsing to detect pull_request_target when on is a string or list (or consider inspecting workflowData.RawFrontmatter["on"] directly), and add a unit test covering the scalar/sequence forms.

Copilot uses AI. Check for mistakes.
contents: read
pull-requests: read
engine: copilot
checkout: false
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

This fixture is used by the integration test's "non-strict mode" subtest (invoked without --strict). In this codebase, strict mode defaults to enabled unless frontmatter sets strict: false, so this fixture will actually run in strict mode by default and emit the dangerous-trigger warning, making the non-strict subtest not exercise the intended behavior. Add strict: false to the frontmatter so the test genuinely covers non-strict mode, while the strict subtest can continue to override it with --strict.

Suggested change
checkout: false
checkout: false
strict: false

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
// The dangerous-trigger warning should also have been emitted before the error.
assert.True(t,
strings.Contains(combined, "very dangerous trigger") ||
strings.Contains(combined, "extremely insecure"),
"output should contain security diagnostics")
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

This assertion is intended to verify the strict-mode output includes the dangerous-trigger warning in addition to the hard error, but as written it will always pass whenever the error message contains "extremely insecure" (which is already asserted above). Tighten this to explicitly require the dangerous-trigger warning text so the test actually validates that both diagnostics are emitted in strict mode.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +27
tests := []struct {
name string
frontmatter string
filename string
strictMode bool
expectError bool
expectWarning bool
errorContains string
warningCount int
}{
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The test case struct includes expectWarning but the test never asserts on it (only warningCount). Either remove expectWarning to reduce confusion, or add an assertion that matches the intended semantic (e.g., warningCount > 0 when expectWarning is true).

Copilot uses AI. Check for mistakes.
…ation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Commit pushed: d38c586

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (522 new lines in pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

AI has analyzed the PR diff and generated a draft ADR to help you get started:

📄 Draft ADR: docs/adr/29433-pull-request-target-security-validation.md

📋 ADR Summary — what was captured

Decision: Add a dedicated validatePullRequestTargetTrigger validation step to the workflow compiler's validatePermissions pipeline that:

  • In non-strict mode: emits a warning when pull_request_target is used without checkout: false
  • In strict mode: always emits a dangerous-trigger warning, and promotes the insecure-checkout case to a hard compile error

Key alternatives documented:

  1. Documentation-only guidance (rejected: passive, no enforcement)
  2. Always hard-error on pull_request_target (rejected: would break valid checkout: false usages)

Normative requirements cover: trigger detection (fast-path + YAML parse), diagnostic rules per mode, error message content, and pipeline integration ordering.

What to do next

  1. Review the draft ADR committed to your branch at docs/adr/29433-pull-request-target-security-validation.md
  2. Complete any [TODO: verify] sections — specifically the Deciders field
  3. Refine the alternatives if you considered other approaches not captured in the draft
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-29433: Security Validation for pull_request_target Trigger

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.


📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🔒 This PR cannot merge until an ADR is linked in the PR body.

References: §25201985048

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 113.2K ·

@pelikhan pelikhan merged commit 1a91c5f into main May 1, 2026
@pelikhan pelikhan deleted the copilot/add-pull-request-target-check branch May 1, 2026 04:31
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.

3 participants