Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/en/rules/untrusted_checkout_exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ severity: error

The workflow appears to checkout untrusted code from a fork and uses a command that is known to allow code execution.

Supported `actions/checkout` releases refuse fork pull request code by default for `pull_request_target` and pull-request-originated `workflow_run` events. Poutine suppresses this finding when that guard applies. The finding remains when the checkout uses `allow-unsafe-pr-checkout: true`, a dynamic value for that input, a known vulnerable commit or a semantic version below its release line's fixed version, or the workflow also uses an event that the guard does not cover. Moving major tags from v2 onward and branch-like refs are assumed to contain the guard.
Supported `actions/checkout` releases refuse fork pull request code by default for `pull_request_target` and pull-request-originated `workflow_run` events. Poutine suppresses this finding when that guard applies. Steps that execute shell scripts from `RUNNER_TEMP` (paths using `$RUNNER_TEMP` or `${RUNNER_TEMP}`) are also suppressed because those files are not part of the checked-out workspace. The finding remains when the checkout uses `allow-unsafe-pr-checkout: true`, a dynamic value for that input, a known vulnerable commit or a semantic version below its release line's fixed version, or the workflow also uses an event that the guard does not cover. Moving major tags from v2 onward and branch-like refs are assumed to contain the guard.

Using workflows with `pull_request_target` has the added benefit (as opposed to `pull_request`) of allowing access to secrets even in forked repositories. There can be good reasons to do so if you need to use API Keys to talk to some external services or want to interact with the GitHub API with `write` permissions. However, this comes at the cost of paying extra attention to the tools you use in your workflow.

Expand Down
6 changes: 6 additions & 0 deletions opa/rego/rules/untrusted_checkout_exec.rego
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ build_commands[cmd] = {
"yarn": {"yarn "},
}[cmd]

# Scripts under RUNNER_TEMP are provisioned by the runner, not the checked-out workspace.
_run_uses_runner_temp_path(run) if {
regex.match(`(?i)(?:\$\{\s*RUNNER_TEMP\s*\}|\$RUNNER_TEMP)/`, run)
}

results contains poutine.finding(rule, pkg_purl, object.union(
{
"path": workflow_path,
Expand All @@ -116,6 +121,7 @@ results contains poutine.finding(rule, pkg_purl, object.union(
sprintf("([^a-z]|^)(%v)", [concat("|", build_commands[cmd])]),
step.run,
)
not _run_uses_runner_temp_path(step.run)
}

results contains poutine.finding(rule, pkg_purl, {
Expand Down
1 change: 1 addition & 0 deletions scanner/inventory_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestGithubWorkflows(t *testing.T) {
".github/workflows/anchors_multiple.yml",
".github/workflows/anchors_with_vulnerability.yml",
".github/workflows/test_new_fields.yml",
".github/workflows/runner_temp_script.yml",
})
}

Expand Down
41 changes: 41 additions & 0 deletions scanner/runner_temp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package scanner

import (
"context"
"testing"

"github.com/boostsecurityio/poutine/models"
"github.com/boostsecurityio/poutine/opa"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRunnerTempScriptNotFlaggedAsUntrustedCheckoutExec(t *testing.T) {
o, err := opa.NewOpa(context.TODO(), &models.Config{
Include: []models.ConfigInclude{},
})
require.NoError(t, err)

i := NewInventory(o, nil, "", "")
pkg := &models.PackageInsights{
Purl: "pkg:github/org/owner",
SourceGitRepo: "org/owner",
SourceGitRef: "main",
}
require.NoError(t, pkg.NormalizePurl())

scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata")
require.NoError(t, err)

for _, finding := range scannedPackage.FindingsResults.Findings {
if finding.RuleId != "untrusted_checkout_exec" {
continue
}
assert.NotEqual(
t,
".github/workflows/runner_temp_script.yml",
finding.Meta.Path,
"bash scripts under RUNNER_TEMP should not be treated as checked-out workspace code",
)
}
}
19 changes: 19 additions & 0 deletions scanner/testdata/.github/workflows/runner_temp_script.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: runner-temp-script
on:
workflow_call:
inputs:
ref:
type: string
required: true
permissions:
contents: read
jobs:
run:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
sparse-checkout: .github
persist-credentials: false
- run: bash "${RUNNER_TEMP}/tooling/setup.sh"