Skip to main content
FieldValue
CategoryCICD-SEC-6
Rule IDcicd-sec-6-secret-in-run-output
SeverityHIGH
OWASPCICD-SEC-6: Insufficient Credential Hygiene
Auto-fix

What the check does

Flags inline run: scripts that expose a ${{ secrets.* }} value, in any of these shapes:
  • a print-family command — echo, printf, print, cat, console.log — whose line interpolates a secret
  • a secret redirected into $GITHUB_OUTPUT or $GITHUB_ENV
  • the legacy ::set-output workflow command carrying a secret
A plain assignment that passes a secret to a step via env: is not flagged — that’s the recommended pattern.

Why it matters

GitHub masks a secret in logs only where it sees the exact registered value. The moment a script prints it (often base64’d, URL-encoded, or concatenated), masking no longer matches and the plaintext lands in logs that anyone with read access to the run can see. Writing a secret to $GITHUB_OUTPUT/$GITHUB_ENV is worse: it persists the value for later steps (and, for outputs, downstream jobs) as plaintext, far outside the step that needed it.

Vulnerable examples

- run: echo "${{ secrets.API_KEY }}"                       # printed to logs
- run: echo "token=${{ secrets.API_KEY }}" >> "$GITHUB_OUTPUT"  # persisted as output
- run: echo "::set-output name=tok::${{ secrets.TOKEN }}"  # legacy output command

Safe alternative

Pass the secret to the consuming step through env: and reference it from the environment — never echo it:
- env:
    API_KEY: ${{ secrets.API_KEY }}
  run: curl --oauth2-bearer "$API_KEY" https://example.com
If a derived value must outlive the step, store it as a masked secret rather than writing plaintext to outputs.