Github Output Injection

GitHub Output File Injection

Description

A step can publish outputs to later steps and jobs by appending key=value lines to the special $GITHUB_OUTPUT file. When a workflow writes user-controllable data (such as github.event.issue.title, github.event.pull_request.body, or github.head_ref) to $GITHUB_OUTPUT, an attacker can inject additional output keys or smuggle newline-delimited payloads that downstream steps consume and act on. 1 This is a variant of the runner environment-file problem described in GitHub Environment File Injection: the data is the same untrusted input covered in Risky Context Usage, but the sink is the step-output channel rather than the environment.

Vulnerable Instance

  • A run: step writes ${{ github.event.* }} (or another user-controllable context) to $GITHUB_OUTPUT.
  • A later step or job reads steps.<id>.outputs.<key> and uses it in a command, condition, or action parameter.
name: Triage
on:
  issues:
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - id: meta
        run: echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
        # A crafted issue title can inject extra output keys, e.g.:
        #   foo\nlabel=critical
      - name: Apply
        run: ./label.sh "${{ steps.meta.outputs.title }}"

Mitigation Strategies

  1. Do not write untrusted context to $GITHUB_OUTPUT Treat ${{ github.event.* }} and ${{ github.head_ref }} as attacker-controlled and never serialize them directly.

  2. Sanitize and validate via an intermediate environment variable Move the value into a step env: variable, validate it, and write it using a delimiter-safe form.

    - id: meta
      env:
        ISSUE_TITLE: ${{ github.event.issue.title }}
      run: |
        if [[ ! "$ISSUE_TITLE" =~ ^[[:alnum:][:space:].,!?/-]+$ ]]; then
          echo "Invalid issue title; rejecting"
          exit 1
        fi
        printf 'title=%s\n' "$ISSUE_TITLE" >> "$GITHUB_OUTPUT"
  3. Validate again at the consumer Steps that read steps.<id>.outputs.<key> should not pass the value unquoted into shell commands; quote it and validate against expected formats.

  4. Use random delimiters for multi-line values For legitimately multi-line outputs use the documented heredoc syntax with an unpredictable delimiter so attacker-supplied newlines cannot terminate the block early.

Secure Version

 name: Triage
 on:
   issues:
 jobs:
   triage:
     runs-on: ubuntu-latest
+    permissions:
+      issues: write
     steps:
       - id: meta
-        run: echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
+        env:
+          ISSUE_TITLE: ${{ github.event.issue.title }}
+        run: |
+          if [[ ! "$ISSUE_TITLE" =~ ^[[:alnum:][:space:].,!?/-]+$ ]]; then
+            echo "Invalid issue title; rejecting"
+            exit 1
+          fi
+          printf 'title=%s\n' "$ISSUE_TITLE" >> "$GITHUB_OUTPUT"
       - name: Apply
-        run: ./label.sh "${{ steps.meta.outputs.title }}"
+        env:
+          TITLE: ${{ steps.meta.outputs.title }}
+        run: ./label.sh "$TITLE"

Impact

DimensionSeverityNotes
LikelihoodMediumCommon in triage and automation workflows that echo event fields into step outputs.
RiskHighPoisoned outputs can alter the behaviour of downstream steps and jobs, including injecting labels, flags, or commands.
Blast radiusModerateLimited to consumers of the output, but those consumers may run with elevated permissions or feed further automation.

References


Last updated on