In this article
Dangerous Workflow Detection
Overview
This page documents the defensive CI control that guards against risky GitHub Actions workflow patterns before merge. The control is a hybrid of two complementary parts:
- A homegrown template-injection gate that runs in PR validation as a fast, deterministic, offline blocking check and surfaces findings in the Security tab as SARIF.
- The Poutine supply-chain scanner, which runs as a broad advisory scanner in CI and uploads its findings to the Security tab as SARIF without blocking merge.
The homegrown gate (blocking)
The homegrown check enforces a single, high-signal rule:
dangerous-workflow/template-injection- Triggered when attacker-controllable GitHub event values are interpolated directly into
run:orgithub-scriptcode execution contexts. The narrowed scope covers free-text and ref fields such asgithub.event.pull_request.title,github.event.pull_request.body,github.event.pull_request.head.ref,github.event.pull_request.head.label,github.event.issue.title,github.event.issue.body,github.event.comment.body,github.event.review.body,github.event.review_comment.body,github.event.discussion.title,github.event.discussion.body,github.event.head_commit.message,github.event.head_commit.author.*,github.event.commits[*].message,github.event.commits[*].author.*,github.event.workflow_run.head_branch,github.event.workflow_run.display_title,github.event.pages[*].page_name, andgithub.head_ref. - Indirect derivations through
steps.*,needs.*, andenv.*are intentionally out of scope to keep the rule deterministic and low-noise.
- Triggered when attacker-controllable GitHub event values are interpolated directly into
This gate is PowerShell-native, has no runtime dependencies, and runs offline as part of
npm run lint:all.
Broad coverage via Poutine (advisory)
Broader dangerous-workflow coverage is delegated to Poutine, an Apache-2.0 supply-chain scanner maintained by BoostSecurity. Poutine runs in CI and reports a wider class of issues as advisory SARIF, including:
- Untrusted checkout / arbitrary code execution from untrusted code changes (
untrusted_checkout_exec). - Template injection across a broader surface than the homegrown gate.
- Unpinned or unverified third-party actions.
- Pull-request jobs running on self-hosted runners.
- Known vulnerabilities in build-platform components.
Poutine findings appear in the Security tab under the poutine category. They are advisory
and do not block merge; the homegrown template-injection gate is the only required check.
Scope and limitations
The split is deliberate:
- The homegrown gate stays narrow, deterministic, and offline so it can block with near-zero false positives.
- Poutine provides breadth and is maintained upstream, but runs advisory to avoid a noisy hard gate.
- The
# poutine:ignore untrusted_checkout_execmarker is honored by Poutine to acknowledge reviewed checkout exceptions. It does not affect the homegrown template-injection gate. - Taint-based expansion of the injection rule (indirect derivations) remains tracked as follow-on work.
Why this exists
This control closes a pre-merge gap. OpenSSF Scorecard can report dangerous workflow findings, but it only runs on the default branch, so a risky workflow can reach the PR branch and merge before the repository sees the issue. The homegrown gate brings a deterministic blocking signal into PR validation, and Poutine adds broad supply-chain coverage on top.
Run it locally
Run the homegrown gate:
npm run lint:dangerous-workflow
The command scans .github/workflows and writes results to the local logs directory. Poutine is a
CI-only scanner and is not part of the offline lint pipeline.
Suppression
Use the suppression marker only when a checkout is genuinely trusted and the exception has been reviewed. The marker is recognized by Poutine on the checkout step line itself or on the immediately preceding line:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # poutine:ignore untrusted_checkout_exec
with:
ref: ${{ github.ref }}
Advisory Poutine findings can also be acknowledged in .poutine.yml by rule, path, or level once
triaged.
Use this only for a legitimate trusted checkout. Review expectations:
- The checkout target must be a trusted constant or otherwise intentionally approved.
- The comment should be added only after a human review confirms that the workflow truly needs the exception.
- Suppressions should be temporary and removed when the workflow is refactored to a safer pattern.
Triage flow
When the required homegrown check fails, resolve the blocking dangerous-workflow/template-injection finding:
- Open the failing check and read the SARIF finding for the affected workflow and line. The homegrown gate emits the stable rule ID
dangerous-workflow/template-injectionatlevel: error. - Locate the
run:orgithub-scriptblock that interpolates an untrusted event value. - Replace the interpolation with a trusted value, route the untrusted value through an intermediate
env:variable, or restructure the workflow so the untrusted payload is never executed as code. - Re-run
npm run lint:dangerous-workflowand re-check the PR validation status.
Advisory Poutine findings such as untrusted_checkout_exec appear separately in the Security tab under the poutine category and do not block merge. Triage them by hardening the workflow or acknowledging the finding in .poutine.yml.
Related documentation
🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.