Skip to main content
FieldValue
CategoryCICD-SEC-6
SeverityHIGH
OWASPCICD-SEC-6: Insufficient Credential Hygiene
Auto-fix✓ (what it does)

What the check does

Two detection paths: 1. Suspicious env: keys with literal values. Any environment variable whose name contains one of token, password, secret, key, webhook, passwd, credential and whose value is a literal (not ${{ secrets.* }} / ${{ ... }}). Checked at workflow, job, and step scope. 2. Pattern-matched literals in run: scripts. Regex matches for:
PatternExample
GitHub PATghp_[A-Za-z0-9]{36}
Slack bot tokenxoxb-...
AWS access keyAKIA[0-9A-Z]{16}
Generic var := "..."token := "abcdef1234567890abcdef"

Why it matters

Hardcoded credentials end up in git history, build logs, action audit logs, and anywhere the workflow file gets mirrored. Rotating them after a leak is painful; preventing the leak is cheaper.

Vulnerable examples

Env var with literal:
jobs:
  deploy:
    env:
      API_TOKEN: "ghp_1234567890abcdefghijklmnopqrstuvwxyz1"   # ← hardcoded
Token in inline script:
- run: |
    curl -H "Authorization: token ghp_1234567890abcdefghijklmnopqrstuvwxyz1" \
      https://api.example.com

Safe alternative

Store the value in GitHub Secrets (or org/environment secrets) and reference it via expression:
jobs:
  deploy:
    env:
      API_TOKEN: ${{ secrets.API_TOKEN }}
    steps:
      - run: |
          curl -H "Authorization: token $API_TOKEN" https://api.example.com

Auto-fix

--fix handles both detection paths: Env-var case. The literal value is replaced with ${{ secrets.<KEY_UPPER> }} — e.g. API_TOKEN: "ghp_..." becomes API_TOKEN: ${{ secrets.API_TOKEN }}. The env-var name itself drives the secrets name. Inline-script case. For each typed literal in the script, the fixer:
  1. Replaces every occurrence with a shell-expandable $ENV_NAME reference, where ENV_NAME is derived from the secret type:
    • GitHub PAT (ghp_*) → $GH_TOKEN
    • Slack token (xoxb-*) → $SLACK_TOKEN
    • AWS access key (AKIA*) → $AWS_ACCESS_KEY_ID
  2. Adds an entry to the step’s env: block: ENV_NAME: ${{ secrets.ENV_NAME }} (creating the block if it doesn’t exist; preserving sibling entries if it does).
Example rewrite:
# before
- run: |
    curl -H "Authorization: token ghp_1234567890abcdefghijklmnopqrstuvwxyz1" https://api.example.com

# after --fix
- run: |
    curl -H "Authorization: token $GH_TOKEN" https://api.example.com
  env:
    GH_TOKEN: ${{ secrets.GH_TOKEN }}
You still need to (a) rotate the leaked credential (it’s in git history) and (b) create a repository or organisation secret with the matching name before the workflow will run cleanly.
The “generic var := "..."” pattern is detected but not auto-fixed — it matches an enclosing key := "value" shape (Go/Make-style assignment), and blindly substituting would corrupt the surrounding syntax. Review and lift manually.