Command Palette

Search for a command to run...

Hectal
Lab 0.3·SecretsHIGH

Masked in the log, stolen anyway

CI 'masks' secrets in logs. A one-line change in a third-party action, or a base64 pipe, prints them in the clear to anyone who can read build logs.

Weakness
CWE-532 · Insertion of sensitive information into log file · CICD-SEC-6
Target
a GitHub Actions workflow in sec-lab
how log masking works (and fails)third-party action riskpinning actions by SHApermissions:zizmor

Run these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.

The threat

  1. 01The workflow under attack

    It looks harmless: a checkout, a popular third-party action pinned to a moving tag, and a deploy step using a long-lived token secret.

    .github/workflows/deploy.ymlwhole fileyaml
    on: push
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: some-org/changed-files@v45        # moving tag
          - run: ./deploy.sh
            env:
              DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
  2. 02Bypass masking with an encoding

    GitHub replaces EXACT secret values in log output with ***. Any transformation (base64, reversing, inserting spaces) produces a string the masker doesn't know. A malicious action, a compromised dependency, or an insider's 'debug' line can print it. Real incident: in March 2025 the widely used tj-actions/changed-files action was compromised, and its tags were re-pointed to code that dumped runner memory secrets into public build logs of thousands of repositories.

    terminal
    $ echo "$DEPLOY_TOKEN" # in a run step
    echo "$DEPLOY_TOKEN" | base64 # same step
    ── output ──
    ***
    Z2hwX2R4N0t2UDJtUTlMcjRUeDhXYjNOYzZWZDFIag==
  3. 03Decode offline

    Anyone with read access to the logs (on a public repo, the whole internet) reverses it.

    terminal
    $ echo Z2hwX2R4N0t2UDJtUTlMcjRUeDhXYjNOYzZWZDFIag== | base64 -d
    ── output ──
    ghp_dx7KvP2mQ9Lr4Tx8Wb3Nc6Vd1Hj

What's at risk

  • The deploy token, with whatever it can do: push code, publish releases, deploy to production.
  • Because the action was referenced by tag, every repo using it was affected the moment the tag moved, with no change on their side.

Detect

  1. 01Static analysis for workflows: zizmor

    zizmor audits GitHub Actions workflows for security problems: unpinned actions, excessive default permissions, template injection, dangerous triggers (pull_request_target, covered in Chapter 3), and credentials persisted by checkout.

    terminal
    $ pipx run zizmor .github/workflows/
    ── output ──
    warning[unpinned-uses]: unpinned action reference
    --> .github/workflows/deploy.yml:7:9
    |
    7 | - uses: some-org/changed-files@v45
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash
    warning[excessive-permissions]: overly broad permissions
    --> .github/workflows/deploy.yml:3:3
    |
    3 | deploy:
    | ^^^^^^ default permissions used due to no permissions: block
    warning[artipacked]: credential persistence through GitHub Actions artifacts
    --> .github/workflows/deploy.yml:6:9
    |
    6 | - uses: actions/checkout@v4
    | ^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false
    3 findings: 0 unknown, 0 informational, 0 low, 3 medium, 0 high
  2. 02Watch for secrets that DO reach logs

    Run gitleaks or trufflehog over downloaded CI logs periodically; they recognise encoded formats of common tokens. GitHub's secret scanning also covers some public logs. Treat any finding as a leak (Lab 0.1's playbook).

Defend

  1. 01Pin, restrict, and stop using long-lived tokens

    Pin third-party actions to a full commit SHA (a tag can be re-pointed; a SHA can't), with the version in a comment so Dependabot can update it. Set permissions: to the minimum; the default token can write to the repo. Don't persist the checkout credential. Replace the static deploy token with OIDC (short-lived, scoped to this repo and environment), so there's no long-lived secret to steal.

    Vulnerable

    .github/workflows/deploy.ymlwhole fileyaml
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: some-org/changed-files@v45
          - run: ./deploy.sh
            env:
              DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

    Hardened

    .github/workflows/deploy.ymlwhole fileyaml
    permissions: {}              # nothing by default
    jobs:
      deploy:
        runs-on: ubuntu-latest
        environment: prod          # approval + env-scoped credentials
        permissions:
          contents: read
          id-token: write          # OIDC only
        steps:
          - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
            with: { persist-credentials: false }
          - uses: some-org/changed-files@<full-40-char-sha> # v45.0.8
          - uses: aws-actions/configure-aws-credentials@<sha> # v4
            with:
              role-to-assume: arn:aws:iam::123456789012:role/sec-lab-deployer
              aws-region: ap-south-1
          - run: ./deploy.sh
  2. 02Keep pins updated automatically

    SHA pins would go stale without automation. Dependabot understands @<sha> # vX.Y.Z and opens PRs that bump both.

    Hardened

    .github/dependabot.ymlwhole fileyaml
    version: 2
    updates:
      - package-ecosystem: github-actions
        directory: /
        schedule: { interval: weekly }

Verify

  1. 01zizmor is clean; there's no token to print

    Even if the base64 trick ran now, there's no DEPLOY_TOKEN in the environment to print, and the AWS credentials from OIDC expire within an hour and only work for this repo's prod environment.

    terminal
    $ pipx run zizmor .github/workflows/
    ── output ──
    No findings to report. Good job!

The concepts

Masking is a convenience, not a control

Log masking prevents ACCIDENTAL exact prints. It can't stop deliberate exfiltration: encoding, splitting, writing to a file uploaded as an artifact, or sending the value over the network to an attacker. Any code running in a job that has a secret can take the secret. The real controls are: fewer secrets (OIDC), less-trusted code never running with them, and short-lived, narrowly scoped credentials.

Third-party actions are dependencies with your secrets

uses: org/action@v4 runs someone else's code, fetched at run time, inside a job that may hold your credentials. A moving tag means that code can change without any change in your repo. Pin to SHAs, prefer actions from verified publishers or your own org, review what they do, and give the job only the permissions that step needs.

Your turn

01

Look up the commit SHA for actions/checkout@v4 and write the pinned uses: line.

02

Which permissions: does a job need to post a comment on a pull request, and nothing else?

Interview questions

01

How do you secure secrets in a CI/CD pipeline?

02

Why pin GitHub Actions to a commit SHA instead of a version tag?

0/4 · 0%