# Potential remote code execution in PyPI

GitHub Actions workflow in pypa/warehouse evaluated untrusted PR branch names/titles in shell and JS contexts, leaking a write-scoped GITHUB_TOKEN and enabling code execution on pypi.org.

- Payout: Research
- Severity: critical
- Program: PyPI
- Write-up by: RyotaK
- Disclosed: 2021-07-30
- Write-up quality: 8/10
- Original: https://blog.ryotak.net/post/pypi-potential-remote-code-execution-en/
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:research sev:critical tech:github-actions technique:chained technique:source-review vuln:command-injection

**Target:** GitHub Actions workflow (combine-prs.yml) in pypa/warehouse
**Root cause:** Untrusted PR branch names and titles were interpolated via ${{ }} into a Bash `run:` step and a JS `script:` step, so expression evaluation happened before shell/JS parsing, allowing command injection.
**Steps:**
1. Fork pypa/warehouse
2. Find an existing branch starting with `dependabot` in upstream
3. Add a harmless commit to that branch in the fork
4. Open a PR titled `;github.auth().then(auth=>console.log(auth.token.split("")))//`
5. Wait for combine-prs.yml to run; the JS script step executes the injected code and prints the token
6. Use leaked GITHUB_TOKEN (write scope) to push to main
7. Auto-deploy pushes code to pypi.org
**How found:** Read Max Justicz's blogpost referencing a PyPI advisory, discovered source in pypa/warehouse, read Pyramid docs and code. Found two low-impact bugs (S3 prefix deletion, role deletion missing project filter). Then read 'Unintended Deployments to PyPI Servers' and realized main-branch pushes auto-deploy, so pivoted to auditing GitHub Actions workflows for write-token access; spotted combine-prs.yml lacking PR author verification and using ${{ }} in run/script steps.
**Exploitation:** Attacker forces the combine-prs workflow to process a crafted PR; the PR title flows into `steps.fetch-branch-names.outputs.prs-string` which is interpolated into a `script:` JS block, executing arbitrary JS in the runner with access to `github.auth()` token. Token has write permission to pypa/warehouse; pushing to main triggers automatic deployment to pypi.org.
**Escalation:** Chained workflow injection -> token leak -> repo write -> auto-deploy to production pypi.org, turning a CI bug into supply-chain RCE.
**Insight:** When a repo auto-deploys from main, audit CI workflows as production attack surface: any `${{ }}` interpolation of PR titles/branch names into `run:` or `script:` blocks is code injection, and `actions/checkout` leaves the write-scoped GITHUB_TOKEN in `.git/config`.
