-
Notifications
You must be signed in to change notification settings - Fork 414
Allow trusted base-branch checkout in pull_request_target validation
#37099
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 all commits
df8ec2f
e111e56
03cf85a
018b7c8
503658d
f5253af
bfe9398
ccdcffc
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 |
|---|---|---|
|
|
@@ -70,6 +70,30 @@ Test workflow content.`, | |
| expectWarning: true, | ||
| warningCount: 2, // 1 for insecure checkout + 1 for sandbox.agent: false | ||
| }, | ||
| { | ||
| name: "pull_request_target with trusted checkout - non-strict - no warnings no error", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| --- | ||
|
|
||
| # PR Target Non-Strict Trusted Checkout | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-trusted-non-strict.md", | ||
| strictMode: false, | ||
| expectError: false, | ||
| expectWarning: false, | ||
| warningCount: 0, | ||
| }, | ||
| { | ||
| name: "pull_request trigger (not target) - non-strict - no diagnostic", | ||
| frontmatter: `--- | ||
|
|
@@ -159,6 +183,155 @@ Test workflow content.`, | |
| errorContains: "pull_request_target trigger with checkout enabled is extremely insecure", | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with explicit checkout pinned to base sha - strict - warning only", | ||
|
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 new test suite has no case for a mixed checkout list — one trusted config and one untrusted config in the same
💡 Suggested addition{
name: "pull_request_target with mixed trusted and untrusted checkouts - strict - errors",
frontmatter: `---
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: read
checkout:
- repository: ${{ github.repository }}
ref: ${{ github.event.pull_request.base.sha }}
- repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
---
# PR Target Mixed Checkouts
Test workflow content.`,
filename: "prt-checkout-mixed-strict.md",
strictMode: true,
expectError: true,
expectWarning: true,
errorContains: "pull_request_target trigger with checkout enabled is extremely insecure",
warningCount: 1,
}, |
||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| --- | ||
|
|
||
| # PR Target Strict Trusted Checkout | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-base-sha-strict.md", | ||
| strictMode: true, | ||
| expectError: false, | ||
| expectWarning: true, | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with explicit checkout default ref in base repo - strict - warning only", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| sparse-checkout: | | ||
| .github | ||
| --- | ||
|
|
||
| # PR Target Strict Trusted Default Ref | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-base-repo-default-ref-strict.md", | ||
| strictMode: true, | ||
| expectError: false, | ||
| expectWarning: true, | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with trusted checkout expressions using compact syntax - strict - warning only", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{github.repository}} | ||
| ref: ${{github.event.pull_request.base.sha}} | ||
| --- | ||
|
|
||
| # PR Target Strict Trusted Compact Expressions | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-trusted-compact-expr-strict.md", | ||
| strictMode: true, | ||
| expectError: false, | ||
| expectWarning: true, | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with explicit checkout pinned to base ref expression - strict - warning only", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| ref: ${{ github.event.pull_request.base.ref }} | ||
| --- | ||
|
|
||
| # PR Target Strict Trusted Base Ref | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-base-ref-strict.md", | ||
| strictMode: true, | ||
| expectError: false, | ||
| expectWarning: true, | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with unwrapped trusted-looking ref string - strict - still errors", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| ref: github.event.pull_request.base.sha | ||
| --- | ||
|
|
||
| # PR Target Strict Unwrapped Ref String | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-unwrapped-ref-string-strict.md", | ||
| strictMode: true, | ||
| expectError: true, | ||
| expectWarning: true, | ||
| errorContains: "pull_request_target trigger with checkout enabled is extremely insecure", | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
|
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] All five new trusted-checkout test cases use In non-strict mode with a trusted checkout the function should return 💡 Suggested addition{
name: "pull_request_target with trusted checkout - non-strict - no warnings no error",
frontmatter: `---
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: read
checkout:
repository: ${{ github.repository }}
ref: ${{ github.event.pull_request.base.sha }}
---
# PR Target Non-Strict Trusted Checkout
Test workflow content.`,
filename: "prt-checkout-trusted-non-strict.md",
strictMode: false, // key: non-strict path
expectError: false,
expectWarning: false,
warningCount: 0,
},With |
||
| { | ||
| name: "pull_request_target with mixed trusted and untrusted checkouts - strict - errors", | ||
| frontmatter: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| tools: | ||
| github: | ||
| toolsets: [pull_requests] | ||
| permissions: | ||
| pull-requests: read | ||
| checkout: | ||
| - repository: ${{ github.repository }} | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| - repository: ${{ github.event.pull_request.head.repo.full_name }} | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| --- | ||
|
|
||
| # PR Target Mixed Checkouts | ||
| Test workflow content.`, | ||
| filename: "prt-checkout-mixed-strict.md", | ||
| strictMode: true, | ||
| expectError: true, | ||
| expectWarning: true, | ||
| errorContains: "pull_request_target trigger with checkout enabled is extremely insecure", | ||
| warningCount: 1, // dangerous-trigger warning | ||
| }, | ||
| { | ||
| name: "pull_request_target with checkout enabled - strict CLI + frontmatter strict false - warning only", | ||
| frontmatter: `--- | ||
|
|
@@ -235,3 +408,72 @@ Test workflow content.`, | |
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMatchesGitHubExpression(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| assertions := []struct { | ||
| name string | ||
| value string | ||
| expected string | ||
| match bool | ||
| }{ | ||
| { | ||
| name: "spaced expression", | ||
| value: "${{ github.repository }}", | ||
| expected: "github.repository", | ||
| match: true, | ||
| }, | ||
| { | ||
| name: "compact expression", | ||
| value: "${{github.repository}}", | ||
| expected: "github.repository", | ||
| match: true, | ||
| }, | ||
| { | ||
| name: "base sha expression matches", | ||
| value: "${{ github.event.pull_request.base.sha }}", | ||
| expected: "github.event.pull_request.base.sha", | ||
| match: true, | ||
| }, | ||
| { | ||
| name: "base ref expression matches", | ||
| value: "${{ github.event.pull_request.base.ref }}", | ||
| expected: "github.event.pull_request.base.ref", | ||
| match: true, | ||
| }, | ||
| { | ||
| name: "head sha expression does not match base sha", | ||
| value: "${{ github.event.pull_request.head.sha }}", | ||
| expected: "github.event.pull_request.base.sha", | ||
| match: false, | ||
| }, | ||
| { | ||
| name: "missing closing braces", | ||
| value: "${{ github.repository", | ||
| expected: "github.repository", | ||
| match: false, | ||
| }, | ||
| { | ||
| name: "empty expression", | ||
| value: "${{}}", | ||
| expected: "github.repository", | ||
| match: false, | ||
| }, | ||
| { | ||
| name: "extra trailing tokens", | ||
| value: "${{ github.repository }}bar}}", | ||
| expected: "github.repository", | ||
| match: 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] Adding cases for those longer expressions would catch any future typo or off-by-one in the expression strings passed to 💡 Suggested additions{
name: "base sha expression matches",
value: "${{ github.event.pull_request.base.sha }}",
expected: "github.event.pull_request.base.sha",
match: true,
},
{
name: "base ref expression matches",
value: "${{ github.event.pull_request.base.ref }}",
expected: "github.event.pull_request.base.ref",
match: true,
},
{
name: "head sha expression does not match base sha",
value: "${{ github.event.pull_request.head.sha }}",
expected: "github.event.pull_request.base.sha",
match: false,
},The third case also documents the intentional rejection of fork-head expressions. |
||
|
|
||
| for _, tc := range assertions { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| actual := matchesGitHubExpression(tc.value, tc.expected) | ||
| if actual != tc.match { | ||
| t.Fatalf("matchesGitHubExpression(%q, %q) = %v, want %v", tc.value, tc.expected, actual, tc.match) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
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] The
[^{}]+?character class in the regex intentionally prevents nested brace expressions from being accepted as trusted. This is a deliberate bypass-hardening choice, but it is implicit.A short comment would help future maintainers understand why braces are excluded from the inner capture — otherwise it looks like an arbitrary restriction rather than a security invariant.
💡 Suggested comment