Skip to content
1Claw Academy
Curriculum/Working with Secrets4 minIntermediate · Lesson 2 of 10

Lab: leak a secret into a build log

Lab

Publish a token to a log and to the process table in four lines, then fix both without changing what the job does.

CI is where secrets leak most often, and almost never through anything exotic. This lab does it two ordinary ways, then closes both. Everything runs locally; no CI provider is involved.

  1. 1

    Write a job that does something entirely reasonable.

    bash
    mkdir -p /tmp/cilab && cd /tmp/cilab
    cat > job.sh <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail
    API_TOKEN="sk-live-9f2a-DO-NOT-LEAK"
    
    echo "--- deploying ---"
    set -x                    # someone added this to debug a failure
    curl -s -o /dev/null "https://example.com/deploy?token=$API_TOKEN" || true
    set +x
    echo "--- done ---"
    EOF
    chmod +x job.sh && ./job.sh
  2. 2

    Read the output. set -x echoes each command after expansion, so the token is now in the build log.

    text
    --- deploying ---
    + curl -s -o /dev/null 'https://example.com/deploy?token=sk-live-9f2a-DO-NOT-LEAK'
    + set +x
    --- done ---
  3. 3

    Harvest it the way anyone with read access to the log would.

    bash
    ./job.sh 2>&1 | grep -o 'sk-live[^ "]*'
  4. 4

    Now the second leak, which people miss entirely. A secret passed as a command-line argument is visible in the process table while the command runs.

    bash
    python3 -c "import time; time.sleep(4)" --token=sk-live-VISIBLE-IN-PS &
    sleep 0.5; ps -o args= -p $!
  5. 5

    The token is right there in the arguments.

    text
    ... Python -c import time; time.sleep(4) --token=sk-live-VISIBLE-IN-PS
  6. 6

    Compare with passing it in the environment instead. Same process, same secret, and nothing in the process table.

    bash
    API_TOKEN=sk-live-HIDDEN python3 -c "import time; time.sleep(4)" &
    sleep 0.5; ps -o args= -p $!
  7. 7

    Fix the log leak by redacting known values before anything is printed. This is what ::add-mask:: does in GitHub Actions and what the official 1Claw action emits before it exports anything.

    bash
    cat > masked.sh <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail
    API_TOKEN="sk-live-9f2a-DO-NOT-LEAK"
    mask() { sed "s|${API_TOKEN}|***|g"; }
    {
      set -x
      curl -s -o /dev/null "https://example.com/deploy?token=$API_TOKEN" || true
      set +x
    } 2>&1 | mask
    EOF
    chmod +x masked.sh && ./masked.sh
  8. 8

    The command still runs and the value is gone from the output.

    text
    + curl -s -o /dev/null 'https://example.com/deploy?token=***'
    + set +x
  9. 9

    Clean up.

    bash
    cd /tmp && rm -rf cilab

Take the two failures separately, because they need different fixes.

  • The log leak is about output, and masking is the answer. It works only for values you have told the masker about, which is why the credential should come from somewhere that knows what it just handed you.
  • The process-table leak is about invocation, and masking cannot help. The fix is to never put a secret in argv: use an environment variable, a header read from a file, or stdin.
Watch out

Masking is a backstop, not a boundary. It catches the value it was given and nothing else, so a token that arrives by a path your masker does not know about is printed in full. Prefer not producing the value over redacting it afterwards.

Tip

Assume the build log is public even on a private repository. Logs get pasted into issues, forwarded to vendors and attached to support tickets, and none of those paths check who is reading.

Nothing in this lab required a vulnerability. A debugging flag someone forgot to remove, and a credential passed the most obvious way, were enough.

Check your understanding

3 questions
1

Why did `set -x` leak the token?

2

Why can masking not fix the process-table leak?

3

Which is the more reliable defence?