From 4ccdb1cc0ab2727567371cf639749de3e4d92b2a Mon Sep 17 00:00:00 2001 From: haoze ying <3246017002@stu.fafu.edu.cn> Date: Thu, 13 Aug 2026 16:36:04 +0800 Subject: [PATCH] :bug: detect explicit unsafe fork checkouts Signed-off-by: haoze ying <3246017002@stu.fafu.edu.cn> --- checks/raw/dangerous_workflow.go | 21 ++++++++- checks/raw/dangerous_workflow_test.go | 67 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/checks/raw/dangerous_workflow.go b/checks/raw/dangerous_workflow.go index 1d8f5ca794f..219b6b575fd 100644 --- a/checks/raw/dangerous_workflow.go +++ b/checks/raw/dangerous_workflow.go @@ -73,6 +73,9 @@ var ( triggerWorkflowRun = triggerName("workflow_run") checkoutUntrustedPullRequestRef = "github.event.pull_request" checkoutUntrustedWorkflowRunRef = "github.event.workflow_run" + checkoutUntrustedHeadRef = "github.head_ref" + checkoutUntrustedForkRepository = "github.event.pull_request.head.repo.full_name" + checkoutAllowUnsafePRInput = "allow-unsafe-pr-checkout" ) // DangerousWorkflow retrieves the raw data for the DangerousWorkflow check. @@ -174,6 +177,21 @@ func createJob(job *actionlint.Job) *checker.WorkflowJob { return &r } +func isExplicitUnsafeForkCheckout(action *actionlint.ExecAction, ref string) bool { + repository, ok := action.Inputs["repository"] + if !ok || repository.Value == nil { + return false + } + allowUnsafe, ok := action.Inputs[checkoutAllowUnsafePRInput] + if !ok || allowUnsafe.Value == nil { + return false + } + + return strings.Contains(repository.Value.Value, checkoutUntrustedForkRepository) && + strings.Contains(ref, checkoutUntrustedHeadRef) && + strings.EqualFold(strings.TrimSpace(allowUnsafe.Value.Value), "true") +} + func checkJobForUntrustedCodeCheckout(job *actionlint.Job, path string, pdata *checker.DangerousWorkflowData, ) error { @@ -202,7 +220,8 @@ func checkJobForUntrustedCodeCheckout(job *actionlint.Job, path string, } if strings.Contains(ref.Value.Value, checkoutUntrustedPullRequestRef) || - strings.Contains(ref.Value.Value, checkoutUntrustedWorkflowRunRef) { + strings.Contains(ref.Value.Value, checkoutUntrustedWorkflowRunRef) || + isExplicitUnsafeForkCheckout(e, ref.Value.Value) { line := fileparser.GetLineNumber(step.Pos) pdata.Workflows = append(pdata.Workflows, checker.DangerousWorkflow{ diff --git a/checks/raw/dangerous_workflow_test.go b/checks/raw/dangerous_workflow_test.go index c6431b7b127..49a37985d62 100644 --- a/checks/raw/dangerous_workflow_test.go +++ b/checks/raw/dangerous_workflow_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/rhysd/actionlint" "go.uber.org/mock/gomock" "github.com/ossf/scorecard/v5/checker" @@ -146,6 +147,72 @@ func TestUntrustedContextVariables(t *testing.T) { } } +func TestValidateUntrustedCodeCheckout(t *testing.T) { + t.Parallel() + + const workflowPrefix = `on: + pull_request_target: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: +` + tests := []struct { + name string + inputs string + expected int + }{ + { + name: "explicit unsafe fork checkout", + inputs: ` repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + allow-unsafe-pr-checkout: true +`, + expected: 1, + }, + { + name: "protected by checkout default", + inputs: ` repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} +`, + expected: 0, + }, + { + name: "head ref without fork repository", + inputs: ` ref: ${{ github.head_ref }} + allow-unsafe-pr-checkout: true +`, + expected: 0, + }, + { + name: "existing pull request expression", + inputs: ` ref: ${{ github.event.pull_request.head.sha }} +`, + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + workflow, errs := actionlint.Parse([]byte(workflowPrefix + tt.inputs)) + if workflow == nil { + t.Fatalf("failed to parse workflow: %v", errs) + } + + data := checker.DangerousWorkflowData{} + if err := validateUntrustedCodeCheckout(workflow, "workflow.yml", &data); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tt.expected, len(data.Workflows)); diff != "" { + t.Errorf("dangerous workflow count mismatch (-want +got):\n%s", diff) + } + }) + } +} + func TestGithubDangerousWorkflow(t *testing.T) { t.Parallel()