A single pull_request_target workflow that checks out a contributor’s branch can turn a harmless documentation pull request into code execution with repository secrets. The dangerous line is often only one line long: ref: ${{ github.event.pull_request.head.sha }}.

That is why a workflow review should not end at “does this action have a pinned version?” Reviewers need to identify what code crosses a trust boundary, what credential it receives, what artifact it can influence, and what evidence remains after a release. This checklist turns those questions into repeatable PR decisions.

Use one named baseline, then add GitHub-specific controls

This article maps controls to the OWASP Top 10 CI/CD Security Risks categories and the joint CISA and NSA Cybersecurity Information Sheet, “Defending Continuous Integration/Continuous Delivery (CI/CD) Environments” (April 2023). OWASP names common failure modes; the CISA/NSA guidance recommends defensive outcomes such as least privilege, protected secrets, isolated environments, artifact integrity checks, and logging.

The GitHub Actions details below are implementation advice, not wording from CISA. For example, CISA recommends least privilege; permissions: contents: read is the GitHub Actions implementation a reviewer can verify in a diff.

Review control OWASP CI/CD category CISA/NSA CIS recommendation supported GitHub Actions evidence to inspect
Restrict triggers, approvals, and deployment paths CICD-SEC-1: Insufficient Flow Control Mechanisms Secure CI/CD pipeline; control and protect pipeline execution on:, environments, required reviewers, branch protection
Minimize token and deployment permissions CICD-SEC-2: Inadequate Identity and Access Management Apply least privilege and strong access control Workflow/job permissions:, environment access, cloud roles
Pin and govern actions, packages, and external services CICD-SEC-3: Dependency Chain Abuse; CICD-SEC-8: Ungoverned Usage of 3rd Party Services Manage dependencies and third-party services securely uses: references, action allowlist, network destinations
Prevent untrusted code from running in privileged workflows CICD-SEC-4: Poisoned Pipeline Execution (PPE) Protect pipeline execution and separate trusted from untrusted work pull_request_target, checkout refs, scripts, generated config
Separate build, approval, and release authority CICD-SEC-5: Insufficient Pipeline-Based Access Controls Control access through the CI/CD pipeline and protect release processes Reusable workflows, environments, protected branches, promotion jobs
Keep secrets out of logs, forks, and build output CICD-SEC-6: Insufficient Credential Hygiene Securely manage credentials and secrets secrets.*, OIDC configuration, shell commands, fork behavior
Harden and isolate runners CICD-SEC-7: Insecure System Configuration Secure and isolate CI/CD infrastructure Runner labels, self-hosted groups, cleanup, network and filesystem access
Verify promoted artifacts and preserve release evidence CICD-SEC-9: Improper Artifact Integrity Validation Validate artifact integrity and protect artifacts Digest, attestation, download source, publish job inputs
Record workflow, approval, and release events CICD-SEC-10: Insufficient Logging and Visibility Implement logging and monitoring Workflow logs, audit logs, deployment records, retained provenance

Start every workflow PR with a five-minute execution map

Before reviewing YAML line by line, write down the workflow’s execution path. This catches flow-control mistakes that a permissions-only review misses. A release workflow with a read-only repository token can still publish a malicious package if its trigger accepts an untrusted artifact.

  1. Identify the trigger: pull_request, pull_request_target, push, workflow_dispatch, workflow_run, or a schedule.
  2. Mark the trust level of every input: PR source, issue comment, branch name, artifact, release tag, matrix value, reusable-workflow input, and downloaded script.
  3. List credentials available to each job: GITHUB_TOKEN, repository secrets, environment secrets, cloud OIDC role, package registry token, or deploy key.
  4. Mark irreversible actions: publishing, deployment, tag creation, release creation, infrastructure changes, or a write to another repository.
  5. Confirm the gate between untrusted work and each irreversible action.

For OWASP CICD-SEC-1: Insufficient Flow Control Mechanisms, the decision rule is simple: an untrusted event must not reach a release-capable job without a trusted gate. A protected GitHub environment with required reviewers is one gate. A separate workflow triggered only from a protected branch is another. “The test job passed” is not a gate if the test job ran contributor-controlled code.

Reject the unsafe pull_request_target checkout pattern

pull_request_target runs in the context of the base repository. That can be useful for safely labeling a pull request or commenting on it, but it becomes dangerous when the workflow checks out and executes the pull request’s head commit. The workflow gains the base repository’s authority while running attacker-controlled code: OWASP CICD-SEC-4: Poisoned Pipeline Execution.

Here is a review diff that should receive a request for changes.

- on: pull_request
+ on: pull_request_target

  jobs:
    test:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
          with:
-           ref: ${{ github.sha }}
+           ref: ${{ github.event.pull_request.head.sha }}
        - run: npm ci
        - run: npm test
          env:
            NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

The checkout line is unsafe because npm ci and npm test can execute repository-controlled lifecycle scripts and test code. Pinning actions/checkout does not fix the problem; the checked-out repository content is the untrusted dependency.

Redesign it as two separate trust boundaries:

on:
  pull_request:

permissions:
  contents: read

jobs:
  test-untrusted-code:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-commit-sha>
      - run: npm ci --ignore-scripts
      - run: npm test

Run contributor tests without repository secrets or write permissions. Then publish only from a protected branch or release tag in a separate workflow. If a pull_request_target workflow is necessary for labeling, do not check out the PR head and do not execute PR-controlled scripts, workflow files, package manifests, or generated configuration.

Review permissions, secrets, and runners as one boundary

OWASP separates CICD-SEC-2: Inadequate Identity and Access Management, CICD-SEC-5: Insufficient Pipeline-Based Access Controls, CICD-SEC-6: Insufficient Credential Hygiene, and CICD-SEC-7: Insecure System Configuration. In GitHub Actions, reviewers should evaluate them together because a privileged token on a shared runner is a materially different risk than the same token on an ephemeral isolated runner.

Set a restrictive default, then grant write permissions only to the job that needs them.

permissions:
  contents: read

jobs:
  publish:
    permissions:
      contents: write
      id-token: write
    environment: production
    runs-on: ubuntu-latest
  • Pass: The default is read-only, the publish job is the only job with write capability, and production uses a protected environment or equivalent approval gate.
  • Request changes: A workflow has permissions: write-all, a broad write token at workflow scope, or secrets are exposed to test jobs that do not publish.
  • Escalation required: A self-hosted runner processes public pull requests and also has cloud credentials, production network access, or persistent workspace state.
  • Documented exception: A legacy deployment needs broad permission temporarily, with an owner, expiry date, compensating review gate, and a tracked remediation issue.

Prefer short-lived cloud credentials obtained through OpenID Connect over a long-lived cloud key stored as a GitHub secret. That is GitHub-specific implementation advice supporting CISA/NSA’s credential-protection recommendation. Also treat self-hosted runners as trust domains: public-fork jobs should not share a runner group with release jobs, and a runner should not retain another repository’s workspace or credentials after completion.

Make artifact promotion a trusted workflow, not a filename convention

OWASP CICD-SEC-9: Improper Artifact Integrity Validation is not solved by naming an artifact release.tar.gz. Artifact names identify a download within a workflow design; they do not prove who built the bytes or whether untrusted code influenced them.

GitHub Actions mechanics matter here. Within the same workflow run, a later job can download an artifact uploaded by an earlier job. Across different workflow runs, a workflow must deliberately locate and download a prior run’s artifact, often through a workflow-run pattern or API-backed action. Across repositories, access requires separate authorization and an explicit download mechanism. In all three cases, “same expected name” is insufficient evidence for promotion.

Use this trust-boundary design:

  • An untrusted PR workflow may build and test an artifact, but it receives no publish credential and cannot trigger release directly.
  • A trusted workflow runs only for a protected branch, protected tag, or an explicitly approved promotion event.
  • The trusted workflow accepts an artifact only when its source run, repository, commit SHA, and expected digest are verified.
  • For release artifacts, require provenance or an attestation that binds the artifact digest to the trusted build identity and source revision.
  • The publish job downloads only the specific approved artifact from the approved run or trusted repository; it must not select “latest” or search by a mutable name.

Separating build and publish credentials alone is not enough. If untrusted code produces dist/app.tar.gz, and a trusted publish job later downloads that artifact merely because its name is app.tar.gz, the untrusted PR still controls what gets released. The required gate is a trusted event or workflow boundary plus verified provenance/digest and constrained artifact-download scope.

Govern actions and leave review evidence behind

Every uses: line introduces a supplier and, frequently, executable code with access to the workspace and token. That maps to OWASP CICD-SEC-3: Dependency Chain Abuse and CICD-SEC-8: Ungoverned Usage of 3rd Party Services. Review JavaScript, container, and composite actions the same way: all can influence commands, files, environment variables, and network traffic.

Require full commit-SHA pinning for third-party actions where your organization’s policy supports it. A tag such as actions/checkout@v4 is easier to read but remains mutable; a full commit SHA identifies one revision. The operational tradeoff is maintenance: someone must update pins and review release notes. That cost is smaller than discovering during an incident that a tag moved after the workflow was approved.

Finally, map OWASP CICD-SEC-10: Insufficient Logging and Visibility to evidence a reviewer can retrieve. Preserve workflow logs, deployment history, environment approvals, release commit SHA, artifact digest, and attestation/provenance records. Avoid printing secrets or dumping full environments; masked values can still leak through transformed output, generated files, or external uploads.

Use this reviewer-ready decision checklist this week

Copy these seven checks into your pull-request template. Each item has a clear reviewer outcome, rather than a vague question.

Checklist item Red flag Acceptable remediation Decision
Trigger and flow control PR, comment, or manual input can reach deployment or publishing directly Separate trusted release workflow; protected branch, tag, or environment approval gate Request changes
Privileged PR execution pull_request_target checks out PR head or runs PR scripts with secrets Use pull_request without secrets; keep target workflow metadata-only Request changes
Token permissions write-all or write permission at workflow scope without need Set contents: read by default; scope write permissions to one job Request changes
Third-party actions New unpinned action, unknown publisher, or unnecessary external service Approved action, full SHA pin, documented owner and purpose Request changes
Secrets and cloud access Secrets available to tests, forks, logs, or persistent runners Job-scoped secrets; OIDC short-lived credentials; isolate untrusted jobs Escalation required for public/self-hosted overlap
Artifact promotion Publish job downloads “latest” or an artifact selected only by name Verify source run, repository, commit, digest, and provenance/attestation Request changes
Audit trail No deployment record, digest, approval evidence, or retained workflow logs Record release SHA, artifact digest, approver, and workflow run URL Pass once evidence is retained

Escalate rather than approving by intuition when a change combines public pull requests, self-hosted runners, production credentials, or cross-repository artifact movement. For lower-risk deviations, approve only as a documented exception with an accountable owner, expiry date, and compensating control. That makes CI/CD security review repeatable: reviewers can point to a trust boundary, an OWASP category, a CISA/NSA recommendation, and a specific GitHub Actions change needed to pass.