← 全部新闻
security5 分钟阅读

GitHub Actions and secrets: the CI/CD mistakes vibe coders make

作者:Seaworthy · 2026年6月25日

Code editor showing HTML and CSS on a monitor screen

Photo by Pankaj Patel on Unsplash

AI writes GitHub Actions workflows fluently. It handles YAML syntax and action references without hesitation, sets up build matrices without complaint. What it consistently gets wrong is secrets handling, and the errors it makes are the kind that do not fail loudly. They fail silently, after your API key has already been exfiltrated.

Where the secrets go

GitHub Actions has a secrets mechanism that looks safe. You store a value in the repository settings, reference it as ${{ secrets.MY_KEY }}, and GitHub masks it in logs. The problem is that masking only works for exact string matches in log output. There are several ways to bypass it without trying.

The most common mistake in AI-generated workflows is passing a secret as an environment variable and then doing something with it that expands or transforms the value.

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - name: Deploy
        run: |
          echo "Deploying with key: ${API_KEY:0:8}..."
          curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy

The slice ${API_KEY:0:8}... prints the first eight characters of your secret. GitHub's masker does not know to mask that substring. It is now in your build logs, readable by anyone with repository access, or by anyone at all if the repo is public.

The curl command is also a risk. If the request fails and curl prints its full command line, the header value lands in the log.

The pull_request_target trap

This one is serious, and AI-generated workflows reproduce it constantly.

pull_request_target was introduced to let workflows comment on PRs from forks. Unlike pull_request, it runs in the context of the base repository, which means it has access to repository secrets. AI assistants often suggest it when you want to add a bot comment or run checks against external PRs, without explaining the trade-off.

on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # checks out attacker code
      - run: npm install && npm test  # runs attacker code with your secrets

That ref line is the problem. You are checking out code from the fork, then executing it in a context that has access to your secrets. An attacker opens a PR, modifies package.json to run a script that exfiltrates secrets.DEPLOY_TOKEN, and your workflow hands it to them.

The fix: never check out or run code from the PR head in a pull_request_target workflow. If you need to comment on a fork PR, do it in a separate job with no access to secrets, triggered by pull_request instead.

Third-party actions are code you run

Every uses: line in your workflow is an instruction to download and execute someone else's code. AI-generated workflows reference third-party actions casually, often without pinning them to a specific commit SHA.

# What AI generates
- uses: some-org/some-action@main

# What you should do
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

The @main reference means the action can change between runs. The maintainer, or an attacker who compromises the maintainer's account, can push new code that reads your secrets and exfiltrates them. Using a commit SHA pins you to a specific, auditable version. Yes, it is more annoying to update. That is the point.

Artifacts and environment dumps

Another pattern in AI-generated workflows is uploading debug artifacts that contain environment variable dumps.

- name: Debug environment
  if: failure()
  run: env > debug.txt

- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: debug-info
    path: debug.txt

env prints everything, including any secrets you have mapped into environment variables. That artifact is now downloadable by anyone with read access to the repository. The if: failure() condition makes it feel like a defensive measure. It is not.

A safer structure

Pin third-party actions to commit SHAs. Use environment-scoped secrets rather than repository-level ones, so each job only has access to what it actually needs. Never pass secrets into env: without checking what that step does with them.

Splitting build and deploy into separate jobs is the most effective structural change:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm ci && npm run build
      # No secrets here

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production  # environment-scoped secrets
    if: github.event_name != 'pull_request'
    steps:
      - run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

The deploy job only runs on non-PR events, which keeps your secrets away from attacker-controlled code. The build job can receive untrusted input without any access to credentials.

For pull_request_target, audit every single workflow that uses it. If it checks out PR code, it is almost certainly wrong.

Seaworthy scans your repository for these patterns, including pull_request_target misuse, unpinned third-party actions, and secrets mapped into environment variables in positions where they can leak.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode