Skip to content

SEC012 — github-env-injection

Severity: blocker · Category: Security

What it checks

Flags a run: step that writes to GITHUB_ENV or GITHUB_PATH using attacker-controllable input (the same untrusted-context list as SEC004), inside a workflow triggered by pull_request_target, workflow_run, or issue_comment.

Why it matters

GITHUB_ENV and GITHUB_PATH let a step set environment variables or prepend to PATH for every step that runs after it, in the same job. In a privileged, dangerous-trigger workflow, an attacker who controls the value being written can set arbitrary environment variables (for example LD_PRELOAD, to hijack dynamic linking in a later step) or add a directory to PATH ahead of the real system tools, letting them shadow a command like ssh or git with their own script.

Examples

Flagged — a comment body, fully attacker-controlled, is written straight into GITHUB_ENV in a privileged trigger context:

on: pull_request_target

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "NOTE=${{ github.event.comment.body }}" >> "$GITHUB_ENV"
      - run: ./build.sh # NOTE is now attacker-controlled in this step's env

Fixed — don't derive GITHUB_ENV/GITHUB_PATH values from untrusted input at all; use GITHUB_OUTPUT (scoped to the producing step, not the whole job) if state needs to pass between steps, or stick to literal, trusted values:

on: pull_request_target

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - id: note
        run: echo "value=fixed-note" >> "$GITHUB_OUTPUT"
      - run: echo "Note was ${{ steps.note.outputs.value }}"

Suppressing

The finding is reported on the run: step doing the write:

      - run: echo "NOTE=${{ github.event.comment.body }}" >> "$GITHUB_ENV" # vlotpipe: ignore[SEC012]

Only if the value is sanitized/validated against a strict allowlist before being written — and even then, prefer fixing the pattern over suppressing it, since this is a blocker-severity finding for a reason.