| Field | Value |
|---|
| Category | CICD-SEC-6 |
| Severity | HIGH |
| OWASP | CICD-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:
| Pattern | Example |
|---|
| GitHub PAT | ghp_[A-Za-z0-9]{36} |
| Slack bot token | xoxb-... |
| AWS access key | AKIA[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:
- 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
- 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.