Secret Exposure Rule

Secret Exposure Rule Overview #

This rule detects excessive secrets exposure patterns in GitHub Actions workflows. It identifies dangerous patterns that expose more secrets than necessary, violating the principle of least privilege.

Key Features #

  • toJSON(secrets) Detection: Identifies when all secrets are exposed at once
  • Dynamic Access Detection: Detects dynamic secret name construction
  • Bracket Notation Warning: Recommends dot notation over bracket notation
  • Multiple Pattern Support: Detects various forms of dynamic secret access
  • CodeQL Compatible: Based on CodeQL’s actions-excessive-secrets-exposure query

Detection Patterns #

The rule triggers when any of the following patterns are detected:

PatternRisk LevelDescription
toJSON(secrets)HighExposes all repository and organization secrets at once
secrets[format(...)]HighDynamically constructs secret names at runtime
secrets[variable]HighUses variables to access secrets dynamically
secrets[object.property]HighUses object properties for dynamic secret selection
secrets['literal']MediumUses bracket notation instead of dot notation

Why This Is Dangerous #

  1. Excessive Exposure: Using toJSON(secrets) or dynamic access patterns exposes more secrets than the workflow actually needs
  2. Attack Surface: If the workflow is compromised, all secrets become accessible to attackers
  3. Audit Difficulty: Dynamic secret access makes it difficult to audit which secrets are actually used
  4. Least Privilege Violation: Workflows should only access the specific secrets they require

Example Vulnerable Workflows #

Example 1: toJSON(secrets) - Exposing All Secrets #

name: Vulnerable Build
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      # BAD: Exposes ALL repository and organization secrets
      ALL_SECRETS: ${{ toJSON(secrets) }}
    steps:
      - name: Build
        run: |
          echo "Building with secrets..."
          # All secrets are now accessible via $ALL_SECRETS

Example 2: Dynamic Secret Access with format() #

name: Multi-Environment Deploy
on: push

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [dev, staging, prod]
    steps:
      - name: Deploy
        env:
          # BAD: Dynamically constructs secret name
          TOKEN: ${{ secrets[format('DEPLOY_TOKEN_%s', matrix.env)] }}
        run: |
          echo "Deploying to ${{ matrix.env }}"

Example 3: Dynamic Access with Variable #

name: Dynamic Secret Access
on: workflow_dispatch

jobs:
  access:
    runs-on: ubuntu-latest
    steps:
      - name: Get secret dynamically
        env:
          SECRET_NAME: API_KEY
          # BAD: Uses variable to access secret
          SECRET_VALUE: ${{ secrets[env.SECRET_NAME] }}
        run: |
          echo "Secret retrieved"

Example 4: Bracket Notation #

name: Bracket Notation
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Use secret
        env:
          # NOT RECOMMENDED: Use secrets.GITHUB_TOKEN instead
          TOKEN: ${{ secrets['GITHUB_TOKEN'] }}
        run: |
          echo "Using token"

Example Output #

$ sisakulint ./vulnerable-workflow.yaml

./vulnerable-workflow.yaml:8:20: excessive secrets exposure: toJSON(secrets) exposes all repository and organization secrets at once. Use specific secret references like secrets.MY_SECRET instead. See https://codeql.github.com/codeql-query-help/actions/actions-excessive-secrets-exposure/ [secret-exposure]
       8 👈|      ALL_SECRETS: ${{ toJSON(secrets) }}

./vulnerable-workflow.yaml:22:18: excessive secrets exposure: secrets[format(...)] dynamically constructs the secret name. This pattern exposes more secrets than necessary and makes security auditing difficult. Use conditional logic with explicit secret references instead. See https://codeql.github.com/codeql-query-help/actions/actions-excessive-secrets-exposure/ [secret-exposure]
      22 👈|          TOKEN: ${{ secrets[format('DEPLOY_TOKEN_%s', matrix.env)] }}

Safe Patterns #

The following patterns are recommended and do NOT trigger warnings:

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      # GOOD: Only access the specific secrets needed
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
    steps:
      - name: Build
        run: npm publish

2. Conditional Logic Instead of Dynamic Access #

on: push

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [dev, staging, prod]
    steps:
      # GOOD: Use conditional logic to select specific secrets
      - name: Deploy to dev
        if: matrix.env == 'dev'
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN_DEV }}
        run: ./deploy.sh

      - name: Deploy to staging
        if: matrix.env == 'staging'
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN_STAGING }}
        run: ./deploy.sh

      - name: Deploy to prod
        if: matrix.env == 'prod'
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN_PROD }}
        run: ./deploy.sh

3. toJSON with Non-Secret Variables #

on: push

jobs:
  debug:
    runs-on: ubuntu-latest
    steps:
      - name: Debug context
        env:
          # GOOD: toJSON with non-secret variables is safe
          MATRIX_JSON: ${{ toJSON(matrix) }}
          GITHUB_JSON: ${{ toJSON(github) }}
          NEEDS_JSON: ${{ toJSON(needs) }}
        run: |
          echo "Matrix: $MATRIX_JSON"

4. Reusable Workflows with Explicit Secrets #

on: push

jobs:
  deploy:
    # GOOD: Pass only the specific secrets needed
    uses: ./.github/workflows/deploy.yml
    secrets:
      deploy_token: ${{ secrets.DEPLOY_TOKEN }}
      npm_token: ${{ secrets.NPM_TOKEN }}

Mitigation Strategies #

  1. Use Explicit References: Always use secrets.SECRET_NAME instead of dynamic access
  2. Conditional Logic: Replace dynamic access with if conditions and explicit secret references
  3. Minimize Scope: Only pass secrets to the steps that actually need them
  4. Audit Usage: Regularly review which secrets each workflow uses
  5. Use Reusable Workflows: Pass only required secrets to reusable workflows

Auto-fix Support #

The secret-exposure rule supports auto-fixing for bracket notation patterns by converting them to dot notation:

# Preview changes without applying
sisakulint -fix dry-run

# Apply fixes
sisakulint -fix on

After auto-fix, bracket notation will be converted to dot notation:

# Before
env:
  TOKEN: ${{ secrets['GITHUB_TOKEN'] }}
  API_KEY: ${{ secrets['MY_API_KEY'] }}

# After auto-fix
env:
  TOKEN: ${{ secrets.GITHUB_TOKEN }}
  API_KEY: ${{ secrets.MY_API_KEY }}

Limitations:

  • Only fixes bracket notation with string literals that are valid identifiers (letters, numbers, underscores)
  • Does not fix dynamic patterns like secrets[format(...)], secrets[variable], or toJSON(secrets)
  • Secret names with hyphens or dots cannot be auto-fixed (they are reported but require manual intervention)

Comparison: Dynamic vs Explicit Access #

ApproachSecurityAuditabilityRecommendation
secrets.MY_SECRETHighEasyRecommended
secrets['MY_SECRET']MediumMediumAvoid
secrets[variable]LowDifficultDo Not Use
secrets[format(...)]LowDifficultDo Not Use
toJSON(secrets)Very LowImpossibleNever Use

OWASP CI/CD Security Risks #

This rule addresses CICD-SEC-2: Inadequate Identity and Access Management by enforcing the principle of least privilege for secrets access in CI/CD pipelines.

See Also #

favicon

codeql.github.com

Using secrets in GitHub Actions - GitHub Docs

favicon

docs.github.com

CICD-SEC-2: Inadequate Identity and Access Management | OWASP Foundation

CICD-SEC-2: Inadequate Identity and Access Management on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.

favicon

owasp.org