Secrets Leaked in CI Logs
Printing or echoing environment variables containing secrets in CI scripts exposes them in build logs, which are often accessible to all repository collaborators and sometimes publicly visible on open-source projects.
How It Works
CI systems like GitHub Actions automatically mask secrets registered in the repository settings when they appear in log output. However, this masking is imperfect. If a secret is echoed indirectly (piped through base64, split across lines, or embedded in a JSON payload), the masking fails and the secret appears in plain text. Additionally, secrets passed as command-line arguments appear in process listings, and secrets written to files may be included in uploaded artifacts. Build logs are retained for 90 days by default and are visible to anyone with repository read access. On public repositories, the logs are visible to everyone on the internet.
# BAD: secrets printed in CI logs in various ways
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
echo "Deploying with key: ${{ secrets.DEPLOY_KEY }}"
echo ${{ secrets.API_TOKEN }} | base64 # masking bypassed!
curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" https://api.example.com
# curl commands show in logs, and the -H value may not be masked
env | grep -i secret # prints all env vars matching 'secret'# GOOD: secrets never printed, used only in masked env vars
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
# Never echo secrets, even for debugging
echo "Deploying to production..."
curl -sf -H "Authorization: Bearer $API_TOKEN" https://api.example.com
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
- name: Validate deployment
run: |
# Use --silent and redirect output to avoid leaking response data
STATUS=$(curl -sf -o /dev/null -w '%{http_code}' https://app.example.com/health)
echo "Health check status: $STATUS"Real-World Example
In the 2021 Codecov breach, attackers modified the Codecov bash uploader script to exfiltrate environment variables (including CI secrets) from customer CI pipelines. The secrets were extracted from the CI environment and sent to an attacker-controlled server. Thousands of organizations had their CI secrets stolen because environment variables containing tokens were available in the CI runner environment and were not properly scoped.
How to Prevent It
- Never echo, print, or log secret values in CI scripts -- not even for debugging purposes
- Use environment variables in env: blocks instead of inline ${{ secrets.* }} interpolation to ensure GitHub's automatic masking works correctly
- Add set +x at the start of shell steps to prevent bash from printing commands as they execute (set -x prints every command including secrets)
- Regularly audit CI logs for accidentally exposed credentials using tools like trufflehog or gitleaks on CI output
Affected Technologies
Data Hogo detects this vulnerability automatically.
Scan Your Repo FreeRelated Vulnerabilities
Unpinned GitHub Actions
highUsing GitHub Actions referenced by mutable tags like @main or @v3 instead of full commit SHAs means a compromised or hijacked action can inject malicious code into your CI pipeline without any change to your workflow file.
GitHub Actions Script Injection
criticalUsing untrusted event data like github.event.issue.title directly inside run: blocks allows attackers to inject arbitrary shell commands into your CI pipeline by crafting malicious issue titles, PR bodies, or commit messages.
Self-Hosted Runner Risks
highUsing self-hosted GitHub Actions runners with pull_request_target or public fork workflows allows untrusted code from external contributors to execute on your infrastructure with access to secrets, persisted state, and the host network.
Overly Permissive Workflow Permissions
mediumGitHub Actions workflows with permissions: write-all or no explicit permissions block grant the GITHUB_TOKEN excessive access, allowing a compromised step to modify code, create releases, write packages, or change repository settings.