Marimo's CVSS 9.3 Pre-Auth RCE and Astral's uv Supply Chain Defense Playbook
Contents
In April 2026, two contrasting events unfolded in the Python ecosystem’s security landscape. On one side, a popular Python notebook tool was found to have a CVSS 9.3 unauthenticated RCE, with actual compromise occurring within 10 hours of the advisory. On the other, Astral, the company behind uv and ruff used by millions of developers, published its defensive posture developed in response to the wave of supply chain attacks in 2026.
Marimo’s CVSS 9.3 Pre-Auth RCE (CVE-2026-39987)
A remote code execution vulnerability allowing unauthenticated shell takeover was discovered in Marimo, an open-source Python notebook. Tracked as CVE-2026-39987 (CVSS v4.0: 9.3), Sysdig’s investigation confirmed active exploitation 9 hours and 41 minutes after advisory disclosure.
What Is Marimo?
Marimo is a reproducibility-focused Python notebook that manages cells as a DAG structure, eliminating the global state problems common in Jupyter. It’s widely used from local analysis and prototyping to team-shared environments exposed on the LAN via --host 0.0.0.0. The project has over 14,000 GitHub stars and roughly 300,000 monthly PyPI downloads.
The Vulnerability
The issue lies in the /terminal/ws WebSocket endpoint in Marimo’s Starlette-based backend.
When Marimo launches in edit mode, it opens a WebSocket server on default port 2718 for the in-browser terminal. Other WebSocket endpoints (such as /ws) call validate_auth() to verify connections, but /terminal/ws skips the authentication check due to an implementation oversight, calling websocket.accept() directly. Once a connection is accepted, it spawns an interactive shell via pty.fork() and hands it to the client.
# Vulnerable code (marimo/_server/api/endpoints/terminal.py)
@router.websocket("/terminal/ws")
async def terminal_ws(websocket: WebSocket, ...) -> None:
if not _terminal_supported():
...
# No validate_auth() call
await websocket.accept() # Accepts without authentication
# Spawns shell via pty.fork()
# Correct implementation (other endpoints)
@router.websocket("/ws")
async def session_ws(websocket: WebSocket, ...) -> None:
await validate_auth(websocket, app_state) # Authentication check here
await websocket.accept()
The CWE is CWE-306 (Missing Authentication for Critical Function). Public deployments without an authentication proxy in front, or instances started with --host 0.0.0.0, are directly affected.
Attack Flow
All an attacker needs is network reachability to the target host. No accounts or tokens required.
sequenceDiagram
participant A as Attacker
participant M as Marimo Server
A->>M: ws://target:2718/terminal/ws<br/>No auth token
M-->>A: validate_auth() skipped<br/>websocket.accept()
M-->>A: pty.fork() → full shell
A->>M: id; whoami; env
M-->>A: uid=0(root) / env vars
A->>M: cat .env
M-->>A: AWS_ACCESS_KEY / DB_PASSWORD etc.
Sysdig’s honeypot observation showed attackers moving through the following sequence: first a marker command for PoC confirmation, then directory enumeration and process inspection, and finally harvesting AWS access keys, DB passwords, and app secrets from the .env file. Sysdig reported the entire reconnaissance-to-exfiltration sequence took roughly 3 minutes.
echo '---POC-START---'
id
echo '---POC-END---'
Notably, at the time of first exploitation, no CVE number had been assigned yet and no public PoC code existed. The attackers weaponized the vulnerability directly from the advisory text alone. This is the type of exploitation that CVE-based threat intelligence cannot detect.
Affected Versions and Exposure
| Item | Details |
|---|---|
| CVE | CVE-2026-39987 / GHSA-2679-6MX9-H9XC |
| CVSS v4.0 | 9.3 (Critical) |
| CVSS v3.1 | 9.5 (Critical) |
| Vulnerable versions | 0.20.4 and below (before 0.23.0) |
| Fixed version | 0.23.0 |
| Internet exposure | ~186 instances, ~16% accepting unauthenticated terminal WS |
Configurations not affected include deployments with an authentication proxy, localhost-only launches bound to 127.0.0.1, and run/application mode. Only edit mode with external exposure is vulnerable.
Similar vulnerability patterns include CVE-2026-22812 (unauthenticated HTTP server) and CVE-2026-22813 (WebSocket RCE) disclosed in March for the AI coding agent OpenCode (/articles/opencode-cve-2026-22812-22813-rce). Langflow’s CVE-2026-33017 also saw active exploitation within 20 hours of disclosure. Dev tools that assume terminal and shell access tend to have blurry authentication boundaries.
Remediation
The fix is available in version 0.23.0.
pip install --upgrade "marimo>=0.23.0"
Network isolation should be done in parallel. Block external access to the default port 2718 with a firewall, and limit shared environments to SSH tunnel or VPN access. If an instance was already publicly exposed, treat all credentials in .env (AWS keys, DB passwords, API tokens, etc.) as compromised and rotate them immediately.
This is also a good time to audit your infrastructure for notebook platforms like Jupyter and Marimo. There’s a strong tendency to think of analysis environments as local-only, and instances inadvertently bound to 0.0.0.0 are more common than expected.
Astral’s Supply Chain Defense for uv and ruff
Astral, the company behind the Python linter ruff and package manager uv, published its supply chain security practices on its official blog. Written by security engineer William Woodruff, the post systematically documents what the team learned from the wave of 2026 supply chain attacks and how they’re defending a toolchain used by millions of developers.
The backdrop for this post is the Trivy/LiteLLM incident. In March 2026, TeamPCP compromised Trivy’s CI/CD pipeline, stole PyPI tokens, and poisoned LiteLLM’s package. The same group also embedded obfuscated payloads via WAV steganography in the telnyx Python SDK. GitHub’s own security roadmap for Actions was published in the same context.
CI/CD Pipeline Defense
The first thing Astral highlights is workflow trigger control in GitHub Actions.
They ban pull_request_target and workflow_run triggers organization-wide. These triggers allow code from external PRs to run with access to repository secrets, making them the breeding ground for “pwn request” attacks. The Ultralytics, tj-actions, and Nx compromises all exploited this trigger type.
A pwn request exploits the pull_request_target trigger to execute attacker-submitted PR code with repository-level permissions. While the regular pull_request trigger doesn’t pass secrets to forked PRs, pull_request_target runs in the base branch context, exposing secrets. Attackers exploit this to steal tokens from CI pipelines.
Action pinning is rigorously enforced. Instead of branches or tags, all actions are pinned to commit hashes and verified with zizmor (a static analysis tool for GitHub Actions) using unpinned-uses and impostor-commit audits. Tags are mutable, and in the Trivy incident, tag rewriting was used to swap in a malicious commit. Hash pinning defeats this technique.
For permission management, the organization default is set to read-only, and all workflows start with permissions: {} before adding only the necessary permissions.
# Astral's policy: start with least privilege
permissions: {}
jobs:
build:
permissions:
contents: read
The External PR Comment Problem and GitHub Apps
Some operations can’t be handled safely in GitHub Actions. Posting comments on external PRs is the prime example.
The pull_request trigger can’t access secrets, so it can’t post comments. Using pull_request_target introduces security risks. Astral resolves this dilemma with a GitHub App (astral-sh-bot). GitHub Apps operate outside workflows, avoiding the code-data conflation of secret exposure within CI pipelines.
That said, GitHub Apps aren’t bulletproof either. An implementation vulnerable to SQL injection or prompt injection turns the app itself into an attack vector. Astral recommends Gidgethub as a framework.
Trusted Publishing to Eliminate Long-Lived Credentials
The core of release security is Trusted Publishing, adopted across PyPI, crates.io, and npm registries.
Trusted Publishing uses OIDC to exchange short-lived tokens between CI providers (like GitHub Actions) and package registries. It completely eliminates long-lived credentials such as API tokens and passwords.
sequenceDiagram
participant GH as GitHub Actions
participant OIDC as GitHub OIDC Provider
participant PyPI as PyPI
participant Fulcio as Sigstore Fulcio
GH->>OIDC: Request OIDC token
OIDC-->>GH: Issue short-lived token<br/>(includes repo & workflow info)
GH->>PyPI: Authenticate with OIDC token<br/>Upload package
PyPI-->>GH: Upload successful
GH->>Fulcio: Request signing cert with OIDC token
Fulcio-->>GH: Issue short-lived X.509 cert
GH->>GH: Sign artifact with ephemeral key pair
GH->>PyPI: Attach Sigstore attestation
OIDC tokens embed information such as repository name, workflow name, and branch. PyPI is configured to accept uploads only from a specific repository’s specific workflow, so even if a token is stolen, it can’t be used from a different workflow.
In the Trivy incident, stolen PyPI API tokens led to LiteLLM’s package being poisoned. With Trusted Publishing, there are no tokens to steal in the first place, eliminating this entire attack path.
Cryptographic Provenance with Sigstore Attestation
On top of Trusted Publishing, Astral layers Sigstore-based attestation. Attestation cryptographically proves provenance: “this binary was built by this workflow, from this commit.”
The standard is PEP 740. Sigstore has three components.
| Component | Role |
|---|---|
| Fulcio | A CA that accepts OIDC tokens and issues short-lived X.509 signing certificates |
| Rekor | A transparency log that records signatures, providing immutable evidence that signing occurred |
| Cosign | A CLI tool for signing and verifying container images and binaries |
The signing key is generated ephemerally and destroyed immediately after signing (keyless signing). This eliminates key management burden entirely. uv’s attestations can be verified on GitHub’s attestations page.
Defense in Depth for the Release Pipeline
Astral’s release process consists of multiple defense layers.
First, GitHub’s immutable releases feature is enabled. In the Trivy incident, force-pushing tags replaced existing release binaries. Immutable releases prohibit modification after release creation, preventing this technique.
Release-time caching is also disabled to prevent GitHub Actions cache poisoning, where malicious files are injected into the cache and used by subsequent builds.
The approval workflow is strict: deployment environments are separated, and access to release secrets requires approval from a different privileged member. For repositories with many release jobs like uv, a deployment protection rule called ost-environment-gate creates a gate from the “release-gate” environment to the “release” environment. Even if a single account is compromised, a release can’t be executed.
Tag protection rulesets are also critical. Release tag creation is blocked until deployment succeeds, and tags become immutable after creation. Releases are only permitted from the main branch.
For standalone installers, checksums are embedded directly in the installer source code so that users vendoring the binaries can verify integrity.
Dependency Management and Cooldowns
Astral uses both Dependabot and Renovate for dependency updates, with an additional “cooldown” period. A cooldown delays updating to a new dependency version for a set period after its release.
Most supply chain attacks cause the most damage immediately after a malicious version is published. The 36 fake Strapi plugins incident was exactly this pattern. Cooldowns buy time for malicious versions to be detected and removed. uv itself has a built-in cooldown feature.
Dependency addition follows a conservative policy: unnecessary features are disabled, dependencies with binary blobs are avoided, and there are plans to reduce compression-scheme-related dependencies. Additionally, Astral invests in relationships with upstream projects, including financial support through the OSS Fund.
Organization-Level Access Control
Administrator account distribution is minimized, and 2FA requires TOTP or better. The long-term goal is migration to phishing-resistant WebAuthn or Passkeys.
Branch protection rules are applied organization-wide, prohibiting force pushes to main. Branch creation matching patterns like advisory-* and internal-* is also blocked, countering the technique of creating branches that impersonate security advisories.
Astral has published these rulesets as GitHub Gists for other OSS projects to reference.
Comparing Against the Trivy Incident
Mapping Astral’s defenses against the Trivy CI/CD compromise shows that nearly every attack technique TeamPCP used is already addressed.
| TeamPCP’s Attack Technique | Astral’s Countermeasure |
|---|---|
Exploited pull_request_target to steal PATs | Organization-wide ban on the trigger |
| Force-pushed tags to swap Actions | Commit hash pinning + immutable releases |
| Poisoned packages with stolen PyPI tokens | Trusted Publishing eliminates tokens entirely |
| Replaced legitimate binaries with malicious ones | Sigstore attestation for cryptographic provenance |
| Released from a single compromised account | Deployment environments requiring multi-member approval |
Marimo’s pre-auth RCE is a “consumer-side” problem with dev tools, while Astral’s supply chain defense is a “producer-side” concern. Both converge on the same outcome: an attacker taking over a Python development environment. Dev tools with misconfigured authentication get exposed to the internet, and poisoned packages flow into CI pipelines. From an attacker’s perspective, development environments are low-cost targets that readily serve as footholds into production.