Skip to content

[Security] Pwn Request in cloudflare-preview.yml — attacker code deployed to Cloudflare Pages #16161

@sourcecodereviewer

Description

@sourcecodereviewer

Summary

The cloudflare-preview.yml workflow uses pull_request_target and checks out the pull request's head commit, then runs npm ci and npm run build on the attacker-controlled code, and deploys the resulting build to Cloudflare Pages. This allows any external attacker to deploy arbitrary JavaScript to a live Cloudflare Pages URL by opening a pull request.

Vulnerability

Workflow: .github/workflows/cloudflare-preview.yml

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

jobs:
  deploy-preview:
    if: github.event.pull_request.head.repo.full_name != github.repository

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}    # attacker's code

      - run: npm ci                                          # executes attacker's package.json hooks
      - run: npm run build                                   # builds attacker's source code

      - uses: cloudflare/wrangler-action@v3                  # deploys attacker's build to Cloudflare
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

The workflow explicitly only runs for fork PRs (head.repo.full_name != github.repository), checks out untrusted PR code, runs the full build pipeline, and deploys the output to Cloudflare Pages using the repository's Cloudflare credentials.

Confirmed Impact

  1. Arbitrary code executionnpm ci runs attacker-controlled preinstall/postinstall hooks in the CI environment
  2. Attacker-controlled JavaScript deployed to live Cloudflare Pages URL — the build output is deployed to https://pr-XXXX.<project>.pages.dev and a comment with the URL is posted on the PR
  3. Cloudflare credentials exposedCLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are used in the same workflow run as attacker code execution
  4. GITHUB_TOKEN with write permissionspull-requests: write and deployments: write

Since this is a healthcare platform frontend, an attacker can inject malicious JavaScript into the deployed preview that targets anyone who visits the preview link (reviewers, QA team, stakeholders).

Suggested Fix

Replace pull_request_target with a two-workflow approach:

  1. A pull_request workflow that builds the PR code without secrets (safe for forks)
  2. A workflow_run workflow that deploys the build artifact only after the first workflow succeeds
# Workflow 1: build.yml (safe, no secrets)
on:
  pull_request:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: build/

# Workflow 2: deploy-preview.yml (privileged, uses secrets)
on:
  workflow_run:
    workflows: ["build"]
    types: [completed]
jobs:
  deploy:
    if: github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          # ...

This ensures attacker code never runs in the same context as Cloudflare credentials.

References

Credit

Discovered by Wilson Cyber Research (@sourcecodereviewer). We request credit in any advisory or fix commit.

Timeline

  • 2026-03-20: Discovered, confirmed via PoC (attacker JS deployed to live Cloudflare URL), all test PRs closed, report filed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions