# Pruva - Complete Reproduction Database # Generated: 2026-08-22T16:33:11.728Z # Total reproductions: 200 This file contains all published vulnerability reproductions from Pruva. For API documentation, see: https://www.pruva.dev/llms.txt ================================================================================ ## REPRO-2026-00319: MariaDB Galera SST remote_auth shell command injection (wsrep_shell_char blacklist bypass) — candidate for v12sec 2026-07-31 0day -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00319 ### Package Information - Name: MariaDB/server - Ecosystem: github - Affected: Unknown - Fixed: Unknown - Severity: critical - CVSS: Unknown - CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) ### Root Cause ## Summary MariaDB Galera's donor-side State Snapshot Transfer (SST) path accepts a joiner-controlled `remote_auth` value and makes it available to `wsrep_sst_mariabackup`. In vulnerable MariaDB 11.8.6, a certificate CommonName supplied by the joining peer becomes the remote username and is interpolated inside a shell command string later executed with Bash `eval`. A CommonName containing a quote and shell control operators therefore escapes the intended `socat` `commonname` argument and executes an arbitrary command under the donor's `mariadbd` OS account. This run confirmed the issue through a real two-node Galera cluster and TCP SST exchange. ## Impact - **Affected package/component:** MariaDB Server with Galera/wsrep enabled, specifically donor-side `wsrep_sst_mariabackup` SST handling and the `remote_auth` data path in `sql/wsrep_sst.cc`. - **Runtime version proven vulnerable:** MariaDB 11.8.6 (`mariadb:11.8.6`, image digest `sha256:78a5047d3ba33975f183f183c2464cc7f1eab13ec8667e57cc9a5821d6da7577`, source revision `9bfea48ce1214cc4470f6f6f8a4e30352cef84e7` as reported by the image). The source identity used for the submitted 11.8.8 context is commit `46a8eb42a520193686d9a16d4cea4b3e002917e4`; it still lacks the strict `remote_auth` allowlist fix. - **Affected version family:** The unsafe donor behavior is present before the MDEV-40056 fix. The ticket identifies 11.8.8 and 10.11.18 as still lacking proposed commit `581562f94a`; this run directly executed the vulnerable path on 11.8.6. - **Risk:** Critical. A party able to join or impersonate a Galera peer and trigger mariabackup SST can execute arbitrary shell commands on a donor as the `mariadbd` service user. This enables database-file access, credential theft, destructive modification, and lateral movement with that account's privileges. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote command/code execution on the donor as the `mariadbd` OS user through a joiner-controlled wsrep SST request. - **Reproduced impact:** A real malicious joiner connected over the Galera TCP boundary, requested mariabackup SST, and caused `id` to run on the donor. The resulting marker contains `uid=999(mysql) gid=999(mysql) groups=999(mysql)`. - **Parity:** `full` - **Not demonstrated:** Privilege escalation beyond the MariaDB service account was not attempted or required. The mysqldump method was not needed for the full impact proof; mariabackup provided the production-path RCE required by the claim. ## Root Cause The vulnerable data flow is: 1. A joining Galera peer connects to the donor through the wsrep TCP protocol and requests SST. 2. The joiner's SST listener prepares an address containing authentication data derived from its TLS certificate CommonName. The donor parses everything before the final `@` as `remote_auth`. 3. In the vulnerable release, `sql/wsrep_sst.cc` splits that value into `auth.remote_name_` and `auth.remote_pswd_` without the strict filename-character allowlist later introduced by MDEV-40056. 4. Donor startup information carries the remote username into `WSREP_SST_OPT_REMOTE_USER` in `wsrep_sst_mariabackup`. 5. The script builds a string such as `...,commonname='$WSREP_SST_OPT_REMOTE_USER'` and passes the composed pipeline to `timeit()`, which executes `eval $cmd`. 6. A remote username like `x';id>/var/lib/mysql/MDEV40056_MARKER;sleep 3;#` closes the intended quote, inserts commands, and comments out the trailing quote. Bash executes `id` as the donor process account. The ticket's intermediate `wsrep_shell_char` blacklist is also structurally unsafe because it permits shell metacharacters such as `;`, `|`, `&`, parentheses, and redirections. More fundamentally, a blacklist is unsuitable for values that may reach shell parsing. On the executed 11.8.6 path there is no effective strict `remote_auth` allowlist before the value reaches the script. The proposed fix is commit [`581562f94a83f29dcf2c6cc761b49ad55d9c287a`](https://github.com/MariaDB/server/commit/581562f94a83f29dcf2c6cc761b49ad55d9c287a). It splits the remote authentication value, validates both user and password with `wsrep_filename_char`, returns `Bad remote auth string. SST canceled.` on any disallowed character, and removes the permissive `wsrep_shell_char` path. Related fixed scripts also stop reading donor authentication from unsafe stdin and avoid placing remote auth into `commonname` for `VERIFY_CA`. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. It honors `PRUVA_ROOT`, uses the prepared repository at `/repo` when available, and otherwise clones into `bundle/artifacts/mariadb-server`. 2. The script pins and verifies the vulnerable MariaDB image, creates a local CA plus donor/joiner certificates, and gives the joiner certificate the malicious CommonName. 3. It starts a real MariaDB/Galera donor, waits for a healthy synchronized node, then starts a real joiner connected to the donor's wsrep TCP listener. The clean join forces mariabackup SST. 4. It polls the donor for the command marker, copies the marker into the bundle, captures donor/joiner logs and loader/linkage evidence, then runs the exact fixed allowlist logic from commit `581562f94a` as a negative control. 5. Expected result: exit status `0`, `bundle/repro/donor_command_marker.txt` shows the `mysql` UID/GID, and the fixed control records `fixed_result=REJECTED` without a marker. ## Evidence - `bundle/logs/vulnerable_donor.log` - Shows a peer connection to the donor's TCP wsrep listener. - Shows `Member 1.0 (joiner) requested state transfer` and `Detected STR version: 1`. - Shows donor execution of `wsrep_sst_mariabackup` and the injected command in the evaluated transport string: ```text commonname='x';id>/var/lib/mysql/MDEV40056_MARKER;sleep 3;#' ``` - `bundle/repro/donor_command_marker.txt` - Contains: ```text uid=999(mysql) gid=999(mysql) groups=999(mysql) ``` - `bundle/logs/vulnerable_joiner.log` - Captures the real joining server, SST listener, and protocol-side state transfer activity. - `bundle/logs/fixed_donor.log` - Records `fixed_result=REJECTED` from the exact `wsrep_filename_char` split/check behavior and includes the fixed source block that emits `Bad remote auth string. SST canceled.` - `bundle/repro/fixed_negative_control.json` - Records the fixed-control process identity, reached validator path, and absence of the marker. - `bundle/logs/source_identity.log` - Binds source commits and immutable Docker image digests. - `bundle/logs/product_linkage.log` - Captures `ldd` output and SHA-256 hashes for `/usr/sbin/mariadbd` and `/usr/lib/galera/libgalera_smm.so`. - `bundle/repro/runtime_manifest.json` - Declares `entrypoint_kind=tcp_peer`, service/health/path flags, source/image identity, and SHA-256 digests of proof artifacts. - Exploit knowledge records created from current-run evidence: - Primitive: `9f8313f4-7255-4327-9080-cb1bb00344ad` - Derived command execution: `7c845cfe-5dab-41d2-9f50-d71160b7cf40` ## Recommendations / Next Steps - Apply or backport the strict validation from commit `581562f94a` to every maintained branch. Validate the username and password independently with a narrow allowlist before storing, exporting, logging, or forwarding them. - Do not pass peer-controlled values through shell command strings. Replace `eval`-based command composition with arrays/direct `exec` invocations so data cannot become shell syntax. - Keep certificate identity verification and authorization distinct from shell command construction; certificate subject fields must always remain data. - Upgrade to a vendor release that explicitly contains the MDEV-40056 fix once available. Do not assume that a version containing earlier SST hardening fully addresses this later `remote_auth` issue. - Restrict Galera/wsrep ports to authenticated cluster members and trusted network segments. Rotate cluster credentials and review donor hosts if an untrusted peer may have joined. - Add regression tests that deliver malicious CommonNames and direct `remote_auth` strings over a real two-node SST flow. Include quotes, semicolons, pipes, ampersands, redirections, parentheses, newline variants, and colon edge cases; verify rejection happens before any SST script starts. ## Additional Notes - The final reproduction script completed successfully twice consecutively after its last modification, and earlier full-path runs independently created the same marker. It cleans containers/networks and repairs bind-mount ownership, making repeated execution idempotent. - The production-path proof is non-sanitized and uses real MariaDB and Galera components rather than a parser/library mock. - The fixed side is a source-bound logic negative control rather than a full fixed server build because the submitted fix is unmerged and not present in a release image. It exercises the exact split and `wsrep_filename_char` predicate and includes the exact fixed source excerpt. The vulnerable impact itself is demonstrated end-to-end over TCP. - The TLS setup is intentionally local and ephemeral. It exists only to make the malicious joiner a trusted certificate holder and exercise the same certificate-CN-to-`remote_auth` path used by the product. ### Reproduction - Reproduced: 2026-08-01T20:29:03.048Z - Duration: 5056s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00319 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00319 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00319/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00319 ================================================================================ ## REPRO-2026-00318: mcp-toolbox authorization bypass: unauthenticated tool invocation via direct HTTP API -------------------------------------------------------------------------------- Status: published Severity: high Type: security ### Identifiers - REPRO ID: REPRO-2026-00318 - CVE: CVE-2026-14537 (https://nvd.nist.gov/vuln/detail/CVE-2026-14537) ### Package Information - Name: googleapis/genai-toolbox - Ecosystem: github - Affected: >= v1.3.0 (2026-05-21) and <= v1.4.0 (2026-06-04) - Fixed: v1.5.0 (2026-06-18) - Severity: high - CVSS: Unknown - CWE: CWE-863 (Incorrect Authorization) (Incorrect Authorization) ### Root Cause # CVE-2026-14537 — Root Cause Analysis ## Summary google/mcp-toolbox (repository `googleapis/genai-toolbox`) versions v1.3.0–v1.4.0 suffer from an incorrect-authorization vulnerability (CWE-863). When the server is configured with an MCP-enabled authorization service (`mcpEnabled: true`, OAuth scopes via `scopesRequired`) **and** the legacy direct HTTP API is enabled (`--enable-api`), the legacy endpoint `POST /api/tool/{toolName}/invoke` executes tools without enforcing the MCP authorization policy. An unauthenticated remote attacker can invoke tools that the operator believes are protected by OAuth scopes, because scope enforcement exists only on the `/mcp` endpoint path. ## Impact - Component: `internal/server/api.go` (`toolInvokeHandler`) together with `internal/server/server.go` (`mcpAuthMiddleware` mounted only under `/mcp`). - Affected versions: v1.3.0 (2026-05-21) through v1.4.0 (2026-06-04). - Fixed in: v1.5.0 (2026-06-18), fix commit `a6ff910a602adece11f0a6581d6211e5927f7182` ("fix(server): fail if MCP auth is enabled together with enable-api (#3435)"). - Risk: high (CVSS 4.0 8.1). Any tool protected solely by the MCP authorization model (an `mcpEnabled` authService plus tool-level `scopesRequired`, with no legacy `authRequired`) is remotely invocable with no credentials at all, including destructive tools (e.g. `sqlite-execute-sql`, SQL execution tools against production databases). ## Impact Parity - Disclosed/claimed maximum impact: authorization bypass — unauthenticated remote invocation of scope-protected tools via the direct HTTP API (`expected_impact=authz_bypass`, surface `api_remote`). - Reproduced impact in this run: identical — HTTP 200 and actual tool execution (arbitrary SQL against the configured SQLite source) via `POST /api/tool/protected-tool/invoke` with no `Authorization` header, while the same unauthenticated caller receives HTTP 401 on `/mcp`. - Parity: `full`. - Not demonstrated: nothing beyond the claimed impact (no code execution was claimed or required). ## Root Cause Authorization in mcp-toolbox v1.3.0/v1.4.0 is split across two independent enforcement points: 1. **MCP path** (`/mcp`): `mcpAuthMiddleware` (`internal/server/server.go`) validates the Bearer token via `ValidateMCPAuth` (including authService `scopesRequired`), and the MCP `tools/call` handler additionally enforces tool-level scopes through `mcputil.ValidateScopes(ctx, tool.GetScopesRequired(), ...)` (`internal/server/mcp/v20250618/method.go`). 2. **Legacy HTTP API path** (`/api`, enabled by `--enable-api`): the router in `internal/server/api.go` has **no** MCP auth middleware, and `toolInvokeHandler` only enforces the legacy `authRequired` mechanism (`tool.Authorized(verifiedAuthServices)`), which returns `true` unconditionally when a tool declares no `authRequired` (`IsAuthorized`: "no authorization requirement"). Tool-level `scopesRequired` is never consulted on this path. Consequently, a tool protected only by the modern MCP scope model (`scopesRequired`, no legacy `authRequired`) is fully open on the legacy HTTP API: the unauthenticated request passes `IsAuthorized([])` and the tool executes. The fix in v1.5.0 does not add scope checks to the legacy endpoint; instead it makes the dangerous configuration fail closed at startup: `cmd/root.go` and `internal/server/server.go` (`InitializeConfigs`) refuse to run when any authService `IsMCPEnabled()` and `EnableAPI` are both set ("MCP Auth cannot be enabled together with the legacy HTTP API"), and a new `IsMCPEnabled()` method was added to the `AuthServiceConfig` interface for that check. Fix commit: `a6ff910a602adece11f0a6581d6211e5927f7182` (PR #3435). ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` (self-contained; installs Go 1.26.3 if needed, clones/uses the prepared repo cache, builds the real product at v1.4.0 and v1.5.0). 2. The script starts a local OIDC authorization-server stub (`bundle/repro/oidc_stub.py`), then launches the real v1.4.0 server with `bundle/repro/tools.yaml` (generic authService `mcpEnabled: true` + `scopesRequired: [read:files]`; tool `protected-tool` of type `sqlite-execute-sql` with `scopesRequired: [execute:sql]` and **no** `authRequired`) and flags `--enable-api --toolbox-url --port 5000`. 3. It then sends the attacker request `POST /api/tool/protected-tool/invoke` with body `{"sql": "SELECT 'CVE-2026-14537-PWNED' AS marker"}` and **no** `Authorization` header, followed by a contrast request to `/mcp` without a token, and finally attempts to start v1.5.0 with the identical config and flags. Expected evidence: - v1.4.0 legacy API: HTTP 200 with the marker `CVE-2026-14537-PWNED` in the JSON response (tool executed unauthenticated) — **vulnerable**. - v1.4.0 `/mcp`: HTTP 401 with a `WWW-Authenticate` challenge — the MCP path enforces authorization correctly. - v1.5.0: exits at startup logging "MCP Auth cannot be enabled together with the legacy HTTP API" and never serves the API — **fixed (fail closed)**. ## Evidence - `bundle/logs/reproduction_steps.log` — full run transcript. - `bundle/logs/vuln_server.log` — v1.4.0 server startup (MCP auth + legacy API both active). - `bundle/logs/vuln_api_invoke_status.txt` / `vuln_api_invoke_body.json` — HTTP status and response body of the unauthenticated invoke (200 + marker). - `bundle/logs/vuln_mcp_noauth_status.txt` / `vuln_mcp_noauth_body.json` — 401 from `/mcp` without a token (contrast control). - `bundle/logs/fixed_server.log` — v1.5.0 startup refusal message. - `bundle/logs/oidc_stub.log` — OIDC discovery requests made by the server at startup (proves the real MCP auth stack was initialized). - `bundle/repro/runtime_manifest.json` — structured runtime evidence manifest. Environment: linux/amd64, Go 1.26.3, mcp-toolbox built from source at tags v1.4.0 (d67cfbe8ddc) and v1.5.0, python3 OIDC stub on 127.0.0.1:8099. ## Recommendations / Next Steps - Upgrade to v1.5.0 or later; do not run `--enable-api` together with MCP-enabled authorization services on affected versions. - Operators on v1.3.0/v1.4.0 who must keep the legacy API should add explicit legacy `authRequired` entries to every tool (the legacy mechanism is still enforced on `/api`), or front the server with a proxy that blocks `/api`. - Long-term: the legacy `/api` endpoints are deprecated; migrate clients to the standard `/mcp` JSON-RPC endpoint where MCP authorization is enforced. ## Additional Notes - The reproduction is idempotent: the script rebuilds only when the resolved commit changes, restarts all services on each run, and cleans up background processes via a trap. - Edge cases: tools that DO declare legacy `authRequired` referencing the mcpEnabled generic authService are *not* bypassed on `/api` (claims come from the MCP context, which is empty there, yielding 401); the bypass applies to tools protected only via the MCP scope model, which is the configuration the fix commit targets ("clients could potentially bypass MCP authorization policies by using the legacy HTTP API"). - No public PoC existed; this reproduction was built from the fix-commit analysis of PR #3435. ### Reproduction - Reproduced: 2026-08-01T05:51:48.163Z - Duration: 2266s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00318 # or: pruva-verify CVE-2026-14537 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00318 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00318/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00318 ================================================================================ ## REPRO-2026-00317: Rails Active Storage variant processing arbitrary file read and potential RCE -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00317 - CVE: CVE-2026-66066 (https://nvd.nist.gov/vuln/detail/CVE-2026-66066) ### Package Information - Name: rails/rails - Ecosystem: github - Affected: activestorage < 7.2.3.2 (Rails 7.0.0-7.2.3.1 affected in default config); 8.0.0-8.0.5; 8.1.0-8.1.3; Rails 6.x only with non-default Active Storage config - Fixed: Unknown - Severity: critical - CVSS: Unknown - CWE: CWE-1188 ### Root Cause # Root Cause Analysis — CVE-2026-66066 (GHSA-xr9x-r78c-5hrm) ## Summary Rails Active Storage's `:vips` variant processor (the default since Rails 7.0) passes attacker-uploaded files to libvips without disabling libvips' "untrusted" (unfuzzed) operations. On the standard Debian/Ubuntu libvips build — the same build shipped by the official `ruby` Docker images and installed by `rails new` generated Dockerfiles — the untrusted `matload` operation (matio + HDF5) is available. An unauthenticated attacker can upload a crafted MATLAB v7.3 (`.mat`/HDF5) file whose matrix data lives in **HDF5 external storage segments pointing at an arbitrary absolute path** on the server (e.g. `/proc/self/environ`). When Active Storage generates an image variant from the upload, libvips loads it with `matload`, and HDF5/matio transparently read the referenced file, returning its bytes as image pixels. The processed variant is served back to the attacker, yielding an **arbitrary file read as the Rails process user**, including the process environment with `SECRET_KEY_BASE`. Active Storage 8.0.5.1 / 7.2.3.2 / 8.1.3.1 fix this by calling `Vips.block_untrusted(true)` at boot (requiring libvips >= 8.13 and ruby-vips >= 2.2.1). ## Impact - Package/component: `activestorage` (Ruby on Rails) variant processing via `ruby-vips`/`image_processing` (`config.active_storage.variant_processor = :vips`, default with `load_defaults 7.0` and later). - Affected versions: activestorage < 7.2.3.2, >= 8.0 < 8.0.5.1, >= 8.1 < 8.1.3.1, with libvips linked against certain third-party libraries (Debian/Ubuntu default builds include matio/HDF5, ImageMagick, poppler, librsvg). - Risk level: critical. Unauthenticated arbitrary file read of any file the Rails process can read (environment secrets, credentials, other users' data). The advisory notes these secrets (especially `secret_key_base`) may enable remote code execution or lateral movement. ## Impact Parity - Disclosed/claimed maximum impact: remote code execution (via arbitrary file read -> secret_key_base -> RCE/lateral movement). - Reproduced impact in this run: **unauthenticated remote code execution** through the production HTTP path, chained as: 1. arbitrary file read — the Rails process environment (`/proc/self/environ`) is exfiltrated byte-exactly through the app's own `resize_to_limit: [100, 100]` variant, leaking `SECRET_KEY_BASE` (canary recovered, two independent attempts); 2. forged signed variation tokens — the leaked secret replicates `Rails.application.message_verifier("ActiveStorage")` offline and mints a variation token carrying an unvalidated `{"instance_eval": ""}` transformation (the `:vips` ImageProcessingTransformer performs no transformation validation; rails issue #56948); 3. code execution — delivering the forged token via `GET /rails/active_storage/representations/redirect///pwn.png` executes the Ruby in the Puma process (unique on-disk markers written in three fresh vulnerable processes; wrong-key control: 404, no marker; fixed 8.0.5.1: chain broken at step 1). - Parity: **full** — unauthenticated RCE on a default-configured app (variant_processor :vips, image_processing 1.x, untrusted uploads with variants displayed), matching the advisory's claimed maximum impact. ## Root Cause 1. **Missing hardening call.** Before the fix, Active Storage never called `Vips.block_untrusted(true)` (libvips >= 8.13) nor set `VIPS_BLOCK_UNTRUSTED`, so every libvips loader/saver flagged `VIPS_OPERATION_UNTRUSTED` ("unfuzzed") remained reachable for attacker-controlled uploads. libvips selects loaders by **content sniffing**, so the web-facing declared MIME type (`blob.content_type in variable_content_types`) does not constrain which loader actually parses the bytes. 2. **A loader that dereferences server-side paths.** The untrusted `VipsForeignLoadMat` (`matload`, via matio) reads MATLAB files; v7.3 `.mat` files are HDF5. HDF5 datasets may keep their payload in **external storage segments** (`H5Pset_external(name, offset, size)`), where `name` may be an absolute path. matio/HDF5 resolve and read those segments transparently, so a crafted dataset's pixel values become the raw bytes of an arbitrary server file. Details that make the payload viable: - libvips `vips__mat_ismat()` only accepts files starting with `MATLAB 5.0` (text prefix at offset 0); - matio's `Mat_Open()` ignores the descriptive text and decides v7.3/HDF5 purely from the header **version field `0x0200`** and endian indicator (`IM` on disk for little-endian), then calls `H5Fopen` (the HDF5 signature lives after the 512-byte user block); - matio requires a `MATLAB_class` attribute stored as a fixed-size, NUL-padded ASCII string. 3. **End-to-end exfil channel.** Active Storage's unauthenticated flow (direct upload -> representations URL) lets the attacker have a variant generated for their own blob: - `POST /rails/active_storage/direct_uploads` returns a `signed_id` for the crafted blob (declared `image/png`; no content verification at upload). `DiskController` skips CSRF protection for the subsequent PUT. - Variation URL tokens (`ActiveStorage.verifier.generate(transformations, purpose: :variation)`) sign **only the transformation hash**, not any blob id, so a token scraped from any public page that renders an image variant can be replayed against the attacker's own `signed_id`. - `RepresentationsController#show` synchronously processes the variant on first request and redirects to the stored PNG. - The ImageProcessing vips pipeline applies a sharpen convolution (`[-1,-1,-1; -1,32,-1; -1,-1,-1]/24`) after thumbnailing. The payload replicates each target byte into three consecutive pixels using **one 1-byte external-storage segment per pixel (3 segments per file byte)** and a 99x1 matrix, so (a) no resampling occurs under `resize_to_limit: [100, 100]`, and (b) each triple's center pixel has all-equal 3x3 neighbours and survives the convolution byte-exactly. One quirk matters for real apps: if the crafted blob is *attached* to a record, the analyzer rewrites `blob.content_type` to the sniffed `application/x-matlab-data`, after which `blob.variable?` is false and variants are refused (`ActiveStorage::InvariableError`). Orphan blobs created by direct upload are never analyzed, so the declared `image/png` stands — this is the path the exploit uses, and it requires no attachment. 4. **The fix.** `activestorage/lib/active_storage/vips.rb` (new in 8.0.5.1) loads ruby-vips at boot and calls `Vips.block_untrusted(true)`, raising at boot when libvips < 8.13 or ruby-vips < 2.2.1 ("unsecurable environment"). With it, `matload` (and svgload, pdfload, magickload, ...) raise `Vips::Error: matload: operation is blocked` — exactly the behavior the fixed build shows in this reproduction. Fix commit range: `v8.0.5...v8.0.5.1` (also 7.2.3.2, 8.1.3.1). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; run with `PRUVA_ROOT=` or from `bundle/repro/`). 2. The script: - installs ruby (>= 3.2), libvips-dev/tools, python3-h5py, sqlite dev headers, bundler; - asserts the environment precondition: libvips >= 8.13 with `matload` present and flagged `untrusted` (Debian/Ubuntu default build); - generates a minimal but realistic Rails app twice — `rails/activestorage 8.0.5` (vulnerable) and `8.0.5.1` (fixed) — with `variant_processor = :vips`, Disk service, an unauthenticated upload form page (CSRF meta tag + a sample image variant URL), and `SECRET_KEY_BASE` sourced from the process environment containing a canary value; - boots each app with Puma (two clean attempts per build); - per attempt, as an unauthenticated attacker: GET / (session + CSRF token + scrape signed variation token), generate the `.mat` payload targeting `/proc/self/environ` at increasing offsets, direct-upload each payload (declared `image/png`), replay the scraped variation token against the attacker's own signed blob id, download the served variant PNG, and decode the exfiltrated bytes; - asserts the vulnerable builds leak `SECRET_KEY_BASE=KINDARAILS2SHELL_...` and the fixed builds fail closed with `Vips::Error (matload: operation is blocked)`. 3. Expected evidence: `RESULT: VULNERABILITY CONFIRMED` with `vulnerable leaks=2/2, fixed blocked=2/2`, exit code 0. ## Evidence - `bundle/logs/reproduction_steps.log` — full run log; key excerpts: - `[deps] vips-8.14.1` / `libvips matload present and marked untrusted` - `* activestorage (8.0.5)` vs `* activestorage (8.0.5.1)`, `image_processing (1.14.0)`, `ruby-vips (2.3.0)` - `[vuln 1] canary SECRET_KEY_BASE recovered from /proc/self/environ` followed by the leaked environment, containing `SECRET_KEY_BASE=KINDARAILS2SHELL_CANARY_...` (both attempts) - `[fixed 1/2] variant processing blocked: Vips::Error (matload: operation is blocked` - `bundle/logs/attempts/vuln_*/leaked_all.raw` — raw exfiltrated `/proc/self/environ` bytes (decoded from the served variant PNGs). - `bundle/logs/attempts/vuln_*/server.log`, `fixed_*/server.log` — Puma/Rails logs of both builds. - `bundle/logs/attempts/vuln_*/chunk_*/du.json`, `pwn.png` — per-chunk direct upload responses and served variant images. - `bundle/repro/runtime_manifest.json` — runtime manifest (entrypoint `endpoint`, service/health/target all true). - Environment: Debian bookworm (ruby:3.4-bookworm container), ruby 3.4.10, libvips 8.14.1 (matio/HDF5 build, `matload` untrusted), matio 1.5.21, HDF5 1.10, no sanitizers (production-path proof). ## Recommendations / Next Steps - Upgrade to activestorage 7.2.3.2 / 8.0.5.1 / 8.1.3.1 (or later) **and** libvips >= 8.13; rotate `secret_key_base` and any credentials present in the application environment. - Stopgap on libvips >= 8.13: set `VIPS_BLOCK_UNTRUSTED=1` or call `Vips.block_untrusted(true)` in an initializer; on libvips < 8.13 remove the ruby-vips dependency entirely. - The same hardening should be considered defense-in-depth for *any* product that runs libvips on untrusted content without `block_untrusted`. - Escalation artifacts: `bundle/repro/escalation_experiments.sh` (runnable), `bundle/logs/escalation/escalation.log`, `rce_marker.txt`, `rce_marker2.txt` (unique markers written by injected code inside fresh Puma processes), `negative_control.json` + `srvNC.log` (wrong-key control, HTTP 404, no marker), `logs/escalation/leak/` (full-environ leak used to recover the complete secret). The second-stage `instance_eval` transformation injection relies on the `:vips` transformer applying no transformation validation (rails issue #56948); it is only reachable to an unauthenticated attacker because variation tokens are signed and the CVE-2026-66066 file read yields the signing secret. - Note for testing: `image_processing` 2.x independently calls `Vips.block_untrusted(true)` when it loads; pin observations accordingly when evaluating exploitability (this reproduction pins 1.14.0 so the only variable is the activestorage version). ## Additional Notes - Idempotency: the script resets each app's DB/storage per attempt and was run twice consecutively in a fresh container; both runs passed (`vulnerable leaks=2/2, fixed blocked=2/2`). - Other untrusted loaders on Debian/Ubuntu libvips (svgload, pdfload, openslideload, magickload, jxlload, jp2kload, fitsload, openexrload, analyzeload, radload, ppmload, csvload, rawload, vipsload, matload) are additional candidate vectors; the ImageMagick route is heavily restricted on Debian/Ubuntu by the `@*` path policy, MVG/MSL stealth registration and `StrictReadImage` nested-coder blocking, while the matload/HDF5 route used here works on the default build. The upstream disclosure notes the reported chain may differ; any single untrusted loader suffices to prove the CVE. - The chunk size (33 bytes/request) is an artifact of defeating the sharpen convolution under `resize_to_limit: [100, 100]`; larger chunks are possible with larger variant limits or format-only variants. - The escalation was additionally validated with negative controls: a forged token signed with a wrong key is rejected (HTTP 404, no marker), and the fixed app (8.0.5.1) blocks the initial file read (`matload: operation is blocked`), leaving the signing key unobtainable. See `bundle/learning/exploit_escalation.json` (outcome: demonstrated) and `bundle/repro/exploit_knowledge.json` for the recorded primitives and the derived command-execution capability. ### Reproduction - Reproduced: 2026-08-01T05:51:29.604Z - Duration: 8922s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00317 # or: pruva-verify CVE-2026-66066 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00317 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00317/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00317 ================================================================================ ## REPRO-2026-00316: marimo Pre-Auth RCE via Terminal WebSocket Authentication Bypass (/terminal/ws missing validate_auth) -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00316 - CVE: CVE-2026-39987 (https://nvd.nist.gov/vuln/detail/CVE-2026-39987) ### Package Information - Name: marimo - Ecosystem: github - Affected: <0.23.0 - Fixed: 0.23.0 - Severity: critical - CVSS: Unknown - CWE: CWE-306 (Missing Authentication for Critical Function) ### Root Cause # RCA Report — CVE-2026-39987: marimo Pre-Auth RCE via /terminal/ws ## Summary marimo's interactive terminal WebSocket endpoint (`/terminal/ws`) completely skipped authentication validation. marimo relies on Starlette's `AuthenticationMiddleware`, which only *marks* failed-auth connections as `UnauthenticatedUser` without actively rejecting WebSocket connections; real enforcement depends on endpoint-level checks. While the main `/ws` endpoint validates credentials, `/terminal/ws` had neither a `@requires("edit")` decorator nor a `validate_auth()` call, so an unauthenticated attacker could open a WebSocket and be handed a full interactive PTY shell running with the privileges of the marimo process — pre-authentication remote code execution. ## Impact - **Package/component:** `marimo` (Python notebook server), `marimo/_server/api/endpoints/terminal.py` — `/terminal/ws` WebSocket endpoint. - **Affected versions:** all versions `< 0.23.0` (verified on `0.22.5`). - **Risk level:** Critical (CVSS 9.3, EPSS 0.953, CISA KEV 2026-04-23, exploited in the wild). Consequences: unauthenticated remote attacker obtains an interactive OS shell with the marimo process's privileges (frequently root in Docker deployments), enabling reconnaissance, credential theft (e.g. `.env` cloud keys), lateral movement, and full host compromise. ## Impact Parity - **Disclosed/claimed maximum impact:** pre-authentication remote code execution (interactive PTY shell, arbitrary OS commands). - **Reproduced impact from this run:** identical — from a raw, credential-less WebSocket client we obtained a PTY shell and executed arbitrary commands (`echo PRUVA_VULN_A_$(id -u)_$(id -un)`), observing marker output `PRUVA_VULN_A1_1000_vscode` proving execution as the marimo server user (uid 1000). - **Parity:** `full`. - **Not demonstrated:** nothing material — the claim is fully reproduced, including the fixed-version negative control. ## Root Cause `marimo/_server/api/endpoints/terminal.py::websocket_endpoint` (vulnerable code at fix-commit parent `c24d4806398f30be6b12acd6c60d1d7c68cfd12a^`) performed only two checks before `websocket.accept()` and `pty.fork()`: 1. `app_state.mode != SessionMode.EDIT` → close. 2. `supports_terminal()` → close. There was **no authentication check**. Because Starlette's `AuthenticationMiddleware` does not reject unauthenticated WebSocket upgrades (it only attaches an `UnauthenticatedUser`), the absence of an explicit `validate_auth(websocket)` call meant anyone could reach the PTY-spawning code. Fix commit `c24d4806398f30be6b12acd6c60d1d7c68cfd12a` (PR #9098, released in 0.23.0) adds exactly: ```python from marimo._server.api.auth import validate_auth ... if app_state.enable_auth and not validate_auth(websocket): await websocket.close(WebSocketCodes.UNAUTHORIZED, "MARIMO_UNAUTHORIZED") return ``` aligning `/terminal/ws` with the auth validation used by the other WebSocket endpoints. Verified in this run: the patch hunk exists at the fixed commit, the parent commit lacks it, the installed 0.22.5 package lacks `validate_auth` in `terminal.py`, and the installed 0.23.0 package contains it. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (helper: `bundle/repro/ws_exploit_client.py`). 2. The script: - clones/uses the marimo source checkout and verifies the fix patch hunk; - creates two virtualenvs: `marimo==0.22.5` (vulnerable) and `marimo==0.23.0` (fixed); - starts each real server with token auth enabled (`marimo edit --headless --token-password topsecretpw`); - proves the HTTP auth gate is active (unauthenticated `/` → HTTP 303 login redirect); - as an unauthenticated attacker, opens a raw WebSocket to `/terminal/ws` (no token/cookie/header) and sends a shell command — **twice** against the vulnerable build (both yield PTY output with the unique marker) and **twice** against the fixed build (both rejected with HTTP 403 during the WS upgrade); - runs an authenticated positive control on the fixed build (valid `access_token` → terminal works), proving the fix blocks only unauthenticated access; - writes `bundle/repro/runtime_manifest.json`. 3. Expected evidence: vulnerable attempts print `RCE_CONFIRMED` with marker `PRUVA_VULN_A__` in PTY output; fixed attempts print `CONNECT_FAILED: InvalidStatus: server rejected WebSocket connection: HTTP 403`. ## Evidence - `bundle/logs/reproduction_steps.log` — full run transcript (verdict line: `vuln RCE attempts OK=2/2, fixed rejects OK=2/2, fixed auth control=1`). - `bundle/logs/server_vuln.log`, `bundle/logs/server_fixed.log` — server startup showing token auth (`URL: http://localhost:2718?access_token=topsecretpw`). - `bundle/logs/vuln_unauth_attempt1.log` / `...attempt2.log` — key excerpt: ``` echo PRUVA_VULN_A1_$(id -u)_$(id -un) vscode ➜ /tmp $ echo PRUVA_VULN_A1_$(id -u)_$(id -un) PRUVA_VULN_A1_1000_vscode RESULT: RCE_CONFIRMED marker observed in PTY output ``` (unauthenticated WS accepted → interactive shell → arbitrary command executed as uid 1000 `vscode`, the marimo process user). - `bundle/logs/fixed_unauth_attempt1.log` / `...attempt2.log` — `CONNECT_FAILED: InvalidStatus: server rejected WebSocket connection: HTTP 403`. - `bundle/logs/fixed_auth_control.log` — authenticated request on the fixed build still obtains the terminal (`PRUVA_FIXED_AUTH_1000_vscode`). - `bundle/logs/patch_hunk.txt` — the added `validate_auth` lines from the fix commit. - `bundle/repro/runtime_manifest.json` — `entrypoint_kind=endpoint`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`. - Environment: Python 3.14.4, pip-installed `marimo==0.22.5` / `marimo==0.23.0`, `websockets` client library, Linux x86_64. Script verified idempotent by two consecutive successful runs (exit 0 both times). ## Recommendations / Next Steps - **Upgrade** to marimo ≥ 0.23.0 immediately (fix: PR #9098 / commit `c24d4806398f30be6b12acd6c60d1d7c68cfd12a`). - **Fix approach (already upstream):** call `validate_auth(websocket)` and close with `WebSocketCodes.UNAUTHORIZED` before `websocket.accept()` whenever `enable_auth` is true — for every WebSocket endpoint, not just `/ws`. - **Defense in depth:** never expose `marimo edit` to untrusted networks; put it behind an authenticating reverse proxy; run it as an unprivileged user; audit any deployment that ran < 0.23.0 with a reachable port for compromise (unexpected PTY child processes, shell history, `.env` access). - **Testing:** add a regression test asserting unauthenticated `/terminal/ws` upgrades are rejected (upstream added one in `tests/_server/api/endpoints/test_terminal.py`). ## Additional Notes - **Idempotency:** the script is fully self-contained (installs its own venvs, manages server lifecycle with bounded waits and process-group cleanup) and passed twice consecutively with exit 0. - **Edge cases:** the vulnerability requires edit mode (`SessionMode.EDIT`, i.e. `marimo edit`, the default) and a POSIX platform with `pty` support — both are the standard deployment shape. Auth must be enabled (non-empty token), which is marimo's default when a token is generated or `--token-password` is set; with auth disabled the impact is identical but by design. - The advisory body text ("<= 0.20.4") understates the range; the structured range `< 0.23.0` is correct — 0.22.5 was confirmed vulnerable here. ### Reproduction - Reproduced: 2026-07-30T07:54:02.195Z - Duration: 1459s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00316 # or: pruva-verify CVE-2026-39987 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00316 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00316/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00316 ================================================================================ ## REPRO-2026-00315: Unauthenticated RCE in ruflo MCP bridge default docker-compose deployment -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00315 - CVE: CVE-2026-59726 (https://nvd.nist.gov/vuln/detail/CVE-2026-59726) ### Package Information - Name: ruflo - Ecosystem: npm - Affected: < 3.16.3 - Fixed: 3.16.3 - Severity: critical - CVSS: Unknown - CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) ### Root Cause # RCA Report — CVE-2026-59726 ## Summary The ruflo MCP bridge (`ruflo/src/ruvocal/mcp-bridge/index.js`, the service built by `ruflo/docker-compose.yml`) exposed `POST /mcp` and `POST /mcp/:group` with **no authentication** and bound to **0.0.0.0** by default. The only blocklist that referenced `terminal_execute` (`AUTOPILOT_BLOCKED_PATTERNS` + `isBlockedTool()`) was enforced solely in the autopilot SSE handler. The shared `executeTool()` function — invoked by `POST /mcp` and `POST /mcp/:group` for every `tools/call` — performed **no gate**, so an unauthenticated network attacker could call `tools/call` → `ruflo__terminal_execute`. The bridge routed that call to the ruflo MCP backend (`@claude-flow/cli`), whose `terminal_execute` handler runs `execSync(command)` on attacker-supplied input, yielding arbitrary command execution **as the `node` user (uid 1000) inside the bridge container**. ## Impact - **Package/component affected:** `ruflo` MCP bridge — `ruflo/src/ruvocal/mcp-bridge/index.js` (the bridge built by `ruflo/docker-compose.yml`, service `mcp-bridge`, port 3001). The dangerous tool implementation lives in the `ruflo` backend (`@claude-flow/cli`, `src/mcp-tools/terminal-tools.ts`, `execSync(command)`). - **Affected versions:** ruflo `< 3.16.3` (vulnerable at main commit `4e18ad84`, the parent of the fix). The default `docker-compose.yml` enabled the `devtools` tool group (`MCP_GROUP_DEVTOOLS=true`), which exposes `terminal_*` tools, and ran the bridge with no `MCP_AUTH_TOKEN` and no bind-host restriction. - **Risk level:** Critical. Unauthenticated remote code execution. From the shell as `node`, an attacker can read every provider API key from the container environment (`OPENAI_API_KEY`, `GOOGLE_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`), spawn attacker-controlled swarms on the victim's keys, and persist poisoned patterns into the AgentDB learning store. ## Impact Parity - **Disclosed/claimed maximum impact:** Unauthenticated remote code execution (shell as `node` / uid 1000) via `POST /mcp` → `tools/call` → `terminal_execute`, with provider API key disclosure and AgentDB poisoning. - **Reproduced impact from this run:** Unauthenticated RCE confirmed end-to-end through the real running bridge container. A single `POST /mcp` `tools/call`/`ruflo__terminal_execute` request with **no authentication header** returned command output `uid=1000(node) gid=1000(node) groups=1000(node)` / `whoami=node`, wrote an attacker marker file to `/tmp` inside the container and read it back, and executed `printenv` for the provider keys (`exitCode: 0`). No API keys were present in the reproduction environment, so the env-leak primitive was exercised but produced empty values; the command-execution primitive is fully demonstrated. - **Parity:** `full` for the core claimed impact (unauthenticated RCE as `node` via `POST /mcp` → `terminal_execute`). The downstream consequences (key theft, swarm abuse, AgentDB poisoning) are direct implications of the demonstrated shell and were not separately exercised. ## Root Cause `createMcpHandler()` (per-group) and the catch-all `POST /mcp` handler both call `executeTool(name, toolArgs)` for `tools/call`. In the vulnerable code `executeTool()` only validated search-query shape and then routed unknown tool names to the matching external MCP backend via `backend.callTool()` — there was **no server-side deny list**. The `AUTOPILOT_BLOCKED_PATTERNS` array (containing `/terminal_execute/`) and `isBlockedTool()` were referenced only inside the autopilot SSE loop, never in `executeTool()`: ```js // vulnerable (4e18ad84) — executeTool() has NO gate: async function executeTool(name, args) { if (!args || typeof args !== "object") args = {}; // ... only search-query validation ... switch (name) { /* search, web_research, guidance */ default: { const activeTools = getActiveTools(); const extTool = activeTools.find(t => t.name === name); if (extTool) { const backend = mcpBackends.get(extTool._backend); if (backend) return backend.callTool(extTool._originalName, args); // -> execSync } }} } ``` The ruflo backend's `terminal_execute` runs the command verbatim: ```js // @claude-flow/cli src/mcp-tools/terminal-tools.ts output = execSync(command, { cwd, encoding: "utf-8", timeout, ... }); ``` Compounding factors in the default deployment: `app.listen(PORT)` binds all interfaces; no auth middleware; CORS `Access-Control-Allow-Origin: *`; the `devtools` group (prefix `terminal_`) is enabled by default; MongoDB bound to `0.0.0.0:27017` without `--auth`. **Fix commit:** `d00a0a40cd8bdbca877ac7f675f416bdc69accd1` (PR #2521, ADR-166 Phase 1–3). It adds a server-side `DANGEROUS_TOOLS` gate at the top of `executeTool()` (denies `terminal_execute` unless `MCP_ENABLE_TERMINAL=true`), a `requireAuth` bearer middleware (`timingSafeEqual`), `BIND_HOST=127.0.0.1` by default with fail-closed on public bind without `MCP_AUTH_TOKEN`, a CORS allowlist, and MongoDB `--auth` defaults. ## Reproduction Steps 1. See `bundle/repro/reproduction_steps.sh` (self-contained, executable). 2. The script resolves the ruflo repo (prepared project cache or fresh clone), checks out the vulnerable commit `4e18ad84` (=`d00a0a40^`) and the fixed commit `d00a0a40` into separate worktrees, sanity-checks that the vulnerable `index.js` lacks `DANGEROUS_TOOLS` and the fixed one has it, builds a real `node:20-slim` container for each commit (the ruflo MCP backend is the real published `ruflo` npm package; `terminal_execute` is verified present before baking), starts each container, and sends the actual unauthenticated `POST /mcp` `tools/call` → `ruflo__terminal_execute` request through the running HTTP service. It runs two clean vulnerable attempts and two clean fixed (negative-control) attempts, then writes `bundle/repro/runtime_manifest.json`. 3. Expected evidence (all under `bundle/`): - `artifacts/http/vuln_attempt1_response.json` — MCP result whose `text` contains `output: "uid=1000(node) ... ... ENV_LEAK:"`, `exitCode: 0` → RCE as `node`. - `artifacts/http/fixed_noauth_response.txt` — `{"error":"unauthorized"}` (HTTP 401). - `artifacts/http/fixed_attempt1_response.json` — `{"error":"Tool ... is disabled by default ...","code":"TOOL_DISABLED"}`; the marker is **absent** (command not executed). - `logs/reproduction_steps.log`, `logs/vuln_container.log`, `logs/fixed_container.log`. ## Evidence Key excerpts (from `bundle/artifacts/http/vuln_attempt1_response.json`, vulnerable bridge, **no Authorization header**): ``` "command": "id; whoami; echo PRUVA_RCE_ > /tmp/PRUVA_RCE_.txt; cat /tmp/PRUVA_RCE_.txt; echo ENV_LEAK:; printenv OPENAI_API_KEY GOOGLE_API_KEY OPENROUTER_API_KEY ANTHROPIC_API_KEY 2>/dev/null || true" "output": "uid=1000(node) gid=1000(node) groups=1000(node)\nnode\nPRUVA_RCE_\nENV_LEAK:\n" "exitCode": 0 ``` Fixed bridge negative control (`bundle/artifacts/http/fixed_attempt1_response.json`, with `Authorization: Bearer ...`): ``` { "error": "Tool \"ruflo__terminal_execute\" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.", "code": "TOOL_DISABLED" } ``` Fixed bridge, no auth (`bundle/artifacts/http/fixed_noauth_response.txt`, HTTP 401): ``` {"error":"unauthorized"} ``` Environment: ruflo repo `ruvnet/ruflo`; vulnerable commit `4e18ad84c6c61be7ef43f62e371f8303a0f7517d`; fixed commit `d00a0a40cd8bdbca877ac7f675f416bdc69accd1`; bridge built from `ruflo/src/ruvocal/mcp-bridge` on `node:20-slim`; ruflo backend = published `ruflo` npm package (via `@claude-flow/cli`), `terminal_execute` confirmed in `tools/list` (331 backend tools, 185 exposed after group filtering). Container runs as `node` (uid 1000). ## Recommendations / Next Steps - Apply ADR-166 (PR #2521) fully: keep the `executeTool()` server-side gate as the single denial point for every path (not just autopilot); keep bearer auth + loopback bind by default; fail-closed on public bind without `MCP_AUTH_TOKEN`; keep `MCP_ENABLE_TERMINAL` opt-in; enforce MongoDB `--auth`. - Operators of any pre-fix exposed instance: firewall `:3001` and `:27017` immediately, rotate all provider API keys, and audit/purge the AgentDB pattern store for injected `agentdb_pattern-store` entries (a patched redeploy does **not** undo poisoning). - Add regression locks (the fix already ships `test-runtime-security.mjs` and `test-security-lock.js`) covering: unauthenticated `POST /mcp` `terminal_execute` → `TOOL_DISABLED`; authenticated call still gated unless `MCP_ENABLE_TERMINAL=true`; public bind without token → non-zero exit. ## Additional Notes - **Idempotency:** `reproduction_steps.sh` was executed three consecutive times; every run exited `0` with `confirmed=true`. It reuses a cached base tar / ruflo prefix / worktrees on a large scratch disk and re-imports fresh images each run. - **Build method note:** The default ruflo Dockerfile runs `npm install -g ruflo`, which resolves an >800 MB dependency tree that exceeds the 1 GB rootless-docker storage here, so a plain `docker build` runs out of space. The script instead assembles the container filesystem on the host's large workspace disk and imports it with `docker import` (single flat layer). The bridge `index.js` is the **unmodified** repo file at each commit, and the ruflo backend is the **real published package** (installed with `--omit=optional`, which still exposes `terminal_execute` because that tool only requires `node:child_process` `execSync`). The opt-in backends (`agentic-flow`, `gemini-mcp-server`, `@openai/codex`) and the `intelligence` (`ruvector`) backend are not part of the vulnerability path (`terminal_execute` is provided by the `devtools`/`ruflo` backend, default-on) and were omitted to fit storage; this does not affect the reproduction. - **Limitation:** No live provider API keys were set in the reproduction environment, so the env-leak output is empty; the `printenv` command executed successfully (demonstrating env access), and key disclosure is a direct implication of the demonstrated shell. ### Reproduction - Reproduced: 2026-07-30T07:51:15.165Z - Duration: 2085s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00315 # or: pruva-verify CVE-2026-59726 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00315 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00315/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00315 ================================================================================ ## REPRO-2026-00314: OpenCTI authentication bypass via user impersonation -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00314 - CVE: CVE-2026-27960 (https://nvd.nist.gov/vuln/detail/CVE-2026-27960) ### Package Information - Name: opencti - Ecosystem: docker - Affected: >= 6.6.0, < 6.9.13 (i.e. 6.6.0 through 6.9.12) - Fixed: 6.9.13 - Severity: critical - CVSS: Unknown - CWE: CWE-287 Improper Authentication (Improper Authentication) ### Root Cause # CVE-2026-27960 — OpenCTI Unauthenticated Authentication Bypass via User Impersonation ## Summary OpenCTI versions 6.6.0 through 6.9.12 contain an improper-authentication flaw (CWE-287) in the GraphQL API bearer-token resolution path. The function `authenticateUserByTokenOrUserId()` in `opencti-platform/opencti-graphql/src/domain/user.js` resolves the HTTP `Authorization: Bearer ` credential against the platform user cache map, which is keyed not only by each user's secret `api_token`, but also by every non-secret identifier of the user: `internal_id`, `standard_id`, and STIX ids (`buildStoreEntityMap()` in `opencti-platform/opencti-graphql/src/database/cache.ts` explicitly pushes `entity.api_token` into the same id list as `internal_id`/`standard_id`). As a result, an unauthenticated remote attacker can present **any known or guessable user identifier** — in particular the hard-coded default-admin `internal_id` `OPENCTI_ADMIN_UUID = 88ec0c6a-13ce-5e39-b486-354fe4a7084f` (`opencti-platform/opencti-graphql/src/schema/general.js`) — as the bearer token and is authenticated as that user without ever proving knowledge of the secret API token, password, or any credential. ## Impact - Package/component affected: `opencti/platform` (OpenCTI GraphQL API, `opencti-graphql`), all deployment modes that expose the HTTP/GraphQL endpoint. - Affected versions: >= 6.6.0, < 6.9.13 (fixed in 6.9.13). - Risk level: critical (CVSS 9.8 per public advisories). An unauthenticated network attacker can query and mutate the GraphQL API as any existing user, including the default admin: full read access to threat-intelligence data and full administrative control (user management, settings, data destruction). ## Impact Parity - Disclosed/claimed maximum impact: unauthenticated remote authentication bypass / authorization bypass allowing API access as any existing user, including the default admin (impact class `authz_bypass`). - Reproduced impact from this run: unauthenticated GraphQL request carrying only the public, hard-coded default-admin `internal_id` as bearer token was accepted by OpenCTI 6.9.12 and executed both `me` (returning the admin identity) and the admin-only `users` listing query. The identical request was rejected on the fixed 6.9.13 build, while the real secret `api_token` remained accepted on both builds. - Parity: `full` (unauthenticated admin impersonation through the production GraphQL boundary demonstrated end-to-end). ## Root Cause `authenticateUserFromRequest()` extracts the bearer value and calls `authenticateUserByTokenOrUserId(context, req, tokenUUID)`. That function only tests `platformUsers.has(tokenOrId)` on the user cache map. `getEntitiesMapFromCache()` builds this map via `buildStoreEntityMap()`, which indexes each user under `internal_id`, `standard_id`, `x_opencti_stix_ids` **and** `api_token`. The code therefore conflates *public identifiers* with *secret credentials*: possession of a user's internal UUID (for the default admin a constant compiled into the shipped source, `OPENCTI_ADMIN_UUID`) is treated as proof of identity. Fix (6.9.13, diff of `src/domain/user.js` between tags 6.9.12 and 6.9.13): the function was split into `authenticateUserByToken()` — which additionally verifies `crypto.timingSafeEqual(Buffer.from(user.api_token), Buffer.from(token))` — and `authenticateUserByUserId()`, which is only reachable after a successfully authenticated header-provider login (`HEADERS_AUTHENTICATORS`), restoring the invariant that a bearer value must be the secret token. - Vendor advisory: https://github.com/OpenCTI-Platform/opencti/security/advisories/GHSA-6vvv-vmfr-xhrx - Fix: `opencti-platform/opencti-graphql/src/domain/user.js` changes between tags 6.9.12 and 6.9.13. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` (self-contained; only needs Docker and network access to pull images). 2. The script: - starts the real dependency stack (Elasticsearch 8.19.16, Redis 7, RabbitMQ 3.13, MinIO) on an isolated Docker network; - starts `opencti/platform:6.9.12` with a configured admin email/password/token, waits for the platform health endpoint (migrations included); - **attack**: POSTs `{"query":"{ me { id name user_email } }"}` to `/graphql` with `Authorization: Bearer 88ec0c6a-13ce-5e39-b486-354fe4a7084f` (the hard-coded default-admin `internal_id`, no credentials); - **attack 2**: POSTs the admin-only `users(first: 5)` listing with the same header; - **controls**: no `Authorization` header, a random unknown UUID bearer, and the real secret `api_token` bearer; - tears the stack down and repeats attack + valid-token control against `opencti/platform:6.9.13` (fixed); - writes `bundle/repro/runtime_manifest.json` and exits 0 only if the vulnerable build impersonates the admin **and** the fixed build rejects the same request. 3. Expected evidence: on 6.9.12 the attack response contains `"user_email":"admin@opencti.io"` for both `me` and `users` queries; controls without a valid secret token return no identity; on 6.9.13 the attack returns no identity while the real token still authenticates. ## Evidence - Driver log: `bundle/logs/reproduction_steps.log` - Attack responses: `bundle/artifacts/opencti/vuln_attack_me_response.json`, `bundle/artifacts/opencti/vuln_attack_users_response.json` - Controls: `bundle/artifacts/opencti/vuln_control_noauth_response.json`, `bundle/artifacts/opencti/vuln_control_random_uuid_response.json`, `bundle/artifacts/opencti/vuln_control_valid_token_response.json` - Fixed-version negative control: `bundle/artifacts/opencti/fixed_attack_me_response.json`, `bundle/artifacts/opencti/fixed_control_valid_token_response.json` - Platform logs: `bundle/artifacts/opencti/platform_vuln.log`, `bundle/artifacts/opencti/platform_fixed.log` - Runtime manifest: `bundle/repro/runtime_manifest.json` - Environment: Docker 29, `opencti/platform:6.9.12` vs `opencti/platform:6.9.13`, Elasticsearch 8.19.16, Redis 7-alpine, RabbitMQ 3.13-management-alpine, MinIO latest. Key excerpts are recorded in `bundle/logs/reproduction_steps.log` (vulnerable build returns the admin identity for the hard-coded UUID bearer; fixed build rejects it). ## Recommendations / Next Steps - Upgrade to OpenCTI >= 6.9.13. - Interim (partial) workaround per vendor: set `APP__ADMIN__EXTERNALLY_MANAGED` to disable the default admin account — note this does not close the bypass for other users, since any user `internal_id`/`standard_id` remains a valid bearer on vulnerable builds. - Treat all user `internal_id`/`standard_id` values as public; rotate admin API tokens if a vulnerable version was exposed. - Regression test: assert that `Authorization: Bearer ` is rejected by the GraphQL endpoint while the user's `api_token` is accepted. ## Additional Notes - The script is idempotent: it recreates the Docker network/containers on each run and cleans them up on exit (trap). It was executed twice consecutively with identical pass results. - No sanitizer, mock, or reimplementation is used: the proof exercises the shipped `opencti/platform` container through its real HTTP/GraphQL listener. - The exploit requires no information beyond what is compiled into the public source tree (`OPENCTI_ADMIN_UUID`), so default deployments are exploitable with zero reconnaissance; impersonating *other* users additionally requires their `internal_id`/`standard_id`, which are routinely exposed in API responses to authenticated parties. ### Reproduction - Reproduced: 2026-07-30T07:51:01.428Z - Duration: 2674s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00314 # or: pruva-verify CVE-2026-27960 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00314 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00314/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00314 ================================================================================ ## REPRO-2026-00312: Gitea diffpatch Git hook installation leads to remote code execution -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00312 - CVE: CVE-2026-60004 (https://nvd.nist.gov/vuln/detail/CVE-2026-60004) ### Package Information - Name: go-gitea/gitea - Ecosystem: github - Affected: >=1.17, <1.27.1 - Fixed: 1.27.1 - Severity: critical - CVSS: Unknown - CWE: CWE-94 (Improper Control of Generation of Code - Code Injection) (Improper Control of Generation of Code ('Code Injection')) ### Root Cause # RCA Report — CVE-2026-60004 / GHSA-rcr6-4jqh-j84m ## Summary Gitea's `POST /api/v1/repos/{owner}/{repo}/diffpatch` endpoint applies attacker-controlled patches inside a **shared bare** temporary clone (`services/repository/files/patch.go` → `TemporaryUploadRepository.Clone(..., bare=true)`). Because the clone is bare, its repository root *is* `$GIT_DIR`. Submitting the same patch twice creates an add/add collision; git's `-3` three-way fallback (enabled for Git ≥ 2.32) then **checks the indexed path out to the working tree** even though the operation uses `--cached`. An executable file placed at `hooks/post-index-change` therefore lands in the live Git hooks directory and becomes an active hook. Git executes it while writing the index, so repository-controlled content runs arbitrary shell commands as the Gitea OS user. With default open registration an unauthenticated visitor can obtain the required write access by registering an account and creating a repository. ## Impact - **Package/component affected:** `services/repository/files/patch.go` (`ApplyDiffPatch`), reached via the public REST endpoint `POST /api/v1/repos/{owner}/{repo}/diffpatch` and the web editor's "apply patch" / cherry-pick fallback paths. Also affects `services/repository/files/cherry_pick.go`. - **Affected versions:** Gitea `>= 1.17` and `< 1.27.1`. - **Risk level:** Critical — remote code execution as the Gitea service account. With open registration (default) the endpoint is reachable by an unauthenticated attacker who self-registers. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote code execution (arbitrary shell command execution as the Gitea OS user), reachable by an unauthenticated attacker via open registration. - **Reproduced impact from this run:** Full remote code execution. The planted `post-index-change` Git hook executed as the Gitea OS user (`vscode` in the test runtime) and wrote a marker file (`RCE_v1.27.0_CONFIRMED`) to disk, reached through the real `POST /api/v1/repos/{owner}/{repo}/diffpatch` endpoint after an unauthenticated self-registration (`/user/sign_up` → HTTP 303) and an ordinary repo-creation flow. - **Parity:** `full` — the claimed unauthenticated→account→repo→RCE chain was exercised end-to-end against the real product binary, with a fixed-version (v1.27.1) negative control that does **not** execute the hook. - **Not demonstrated:** Nothing; the code-execution outcome itself was demonstrated (not merely a crash). ## Root Cause `ApplyDiffPatch` prepares the patch in a temporary clone via `TemporaryUploadRepository.Clone(ctx, opts.OldBranch, /*bare=*/true)` with `Shared: true`. The apply command is: ```go cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary") if git.DefaultFeatures().CheckVersionAtLeast("2.32") { cmdApply.AddArguments("-3") // --3way fallback } ``` Step-by-step: 1. **Call #1** — a clean patch that adds `hooks/post-index-change` (mode `100755`). With `--cached` the file only enters the index; no working-tree file is written. The resulting commit (tree containing `hooks/post-index-change`) is pushed to the repository, so `HEAD` now tracks that path. 2. **Call #2** — the *same* patch is submitted again. `SetDefaultIndex` (`git read-tree HEAD`) loads the index from the new `HEAD`, which already contains `hooks/post-index-change`. Re-applying the "add" patch produces an **add/add collision**. The `-3` three-way fallback then checks the indexed path out to the working tree even though `--cached` was requested. In a **bare** clone the working tree *is* `$GIT_DIR`, so the executable file is written to `$GIT_DIR/hooks/post-index-change` — a live Git hook. 3. While git writes the index during the apply/merge, it invokes the `post-index-change` hook, which executes the attacker's shell commands as the Gitea OS user. The hook's exit value is not propagated to the diffpatch HTTP response. **Fix (v1.27.1, PR #38637/#38638 "refactor: git patch apply"):** the temporary clone is no longer bare — `Clone(ctx, opts.OldBranch, /*bare=*/false)`. With a real working tree, the three-way fallback writes the checked-out path into the worktree (not `$GIT_DIR/hooks`), so no hook is installed and the apply fails closed (`git apply error: ... hooks/post-index-change: patch does not apply`, HTTP 500) instead of executing attacker code. ## Reproduction Steps 1. Reference: `bundle/repro/reproduction_steps.sh` (self-contained; downloads the official Gitea linux-amd64 binaries for the vulnerable `1.27.0` and the fixed `1.27.1` builds). 2. What the script does, for each build: - Starts a fresh Gitea instance (SQLite, open registration) on localhost as the current OS user and waits for the `/api/v1/version` healthcheck. - **Unauthenticated step:** `GET /user/sign_up` (verifies the open-registration form is served) then `POST /user/sign_up` to self-register an account (HTTP 303 = success); confirms the new account authenticates via the API (`GET /api/v1/user` with basic auth → HTTP 200). - Creates a repository `exploit-repo` with `auto_init` (establishes the `main` branch). - Builds a malicious patch that adds an executable file `hooks/post-index-change` whose body writes a version-specific marker file and records `id -un`. - **Call #1:** `POST /api/v1/repos/{owner}/exploit-repo/diffpatch` with the patch (clean apply, HTTP 201). - **Call #2:** the *same* patch again (add/add collision → three-way fallback → hook planted and triggered). - Checks for the RCE marker file (written by the hook as the Gitea OS user). 3. Expected evidence: on the vulnerable build the marker file `RCE_v1.27.0_CONFIRMED` is created and `vuln_rce_hook.log` records `hook_ran_as_user=vscode`; on the fixed build Call #2 returns HTTP 500 with `git apply error: ... hooks/post-index-change: patch does not apply` and no marker is created. ## Evidence All artifacts under `bundle/` (relative to the bundle root): - `bundle/repro/reproduction_steps.sh` — the reproducer. - `bundle/repro/runtime_manifest.json` — runtime manifest (entrypoint_kind `endpoint`, service_started/healthcheck_passed/target_path_reached all true). - `bundle/logs/repro/gitea_vuln_stdout.log` / `gitea_fixed_stdout.log` — Gitea server logs for each build. - `bundle/logs/repro/vuln_registration.txt` — `registration_http=303`, `api_auth_http=200` (unauthenticated→account chain). - `bundle/logs/repro/vuln_signup_page.html` — the served open-registration form. - `bundle/logs/repro/vuln_patch.txt` — the malicious patch payload. - `bundle/logs/repro/vuln_call1_response.json` / `vuln_call2_response.json` — diffpatch API responses (both HTTP 201 on the vulnerable build). - `bundle/logs/repro/vuln_diffpatch_calls.txt`: `call1_http=201 call2_http=201 marker_found=yes gitea_run_user=vscode`. - `bundle/logs/repro/vuln_rce_marker.txt` — `RCE_v1.27.0_CONFIRMED` (written by the executed hook). - `bundle/logs/repro/vuln_rce_hook.log` — `hook_ran_as_user=vscode` (twice, once per index write). - `bundle/logs/repro/fixed_call2_response.json` — `{"message":"git apply error: exit status 1 - Performing three-way merge... error: hooks/post-index-change: does not match index ... patch does not apply"}` (HTTP 500, hook NOT installed). - `bundle/logs/repro/fixed_diffpatch_calls.txt`: `call1_http=201 call2_http=500 marker_found=no`. Environment: official Gitea `1.27.0` / `1.27.1` linux-amd64 binaries, SQLite backend, Git 2.55.0 (≥ 2.32, so `-3` three-way fallback active), x86_64 Linux, gitea running as the `vscode` OS user. Key excerpts: ``` [vuln] Registration POST HTTP=303 (303 redirect = success) [vuln] API basic-auth as intruder_vuln: HTTP=200 [vuln] Call #1 HTTP=201 [vuln] Call #2 HTTP=201 [vuln] *** RCE MARKER FILE CREATED BY GIT HOOK *** [vuln] marker content: RCE_v1.27.0_CONFIRMED vuln_rce_hook.log: hook_ran_as_user=vscode [fixed] Call #2 HTTP=500 [fixed] No RCE marker file present at /tmp/gitea_rce_marker_v1.27.1 fixed_call2_response.json: "git apply error: exit status 1 - Performing three-way merge... error: hooks/post-index-change: does not match index ... patch does not apply" ``` ## Recommendations / Next Steps - **Upgrade to Gitea 1.27.1** (or later), which makes the temporary patch clone non-bare so checked-out paths cannot land in `$GIT_DIR/hooks`. - Defense-in-depth: do not run `git apply --index` against a bare repository at all; avoid combining `--cached` with `--index`/`--3way` semantics on a bare clone; consider `core.hooksPath` isolation / disabling `post-index-change` for internal temporary clones. - Restrict `service.DISABLE_REGISTRATION` / require admin approval on internet-facing instances to remove the unauthenticated reachability path. - Add a regression test asserting that the temporary patch repository is non-bare (the upstream fix added `services/repository/files/patch_test.go` `TestGitPatchPrepare` checking for `basePath/.git`). ## Additional Notes - **Idempotency:** confirmed — the script was run twice consecutively; both runs confirmed the vulnerable build (marker created, hook ran as the gitea OS user) and cleared the fixed build (no marker). The script removes prior per-run state and markers at start, so it is safe to re-run. - The `post-index-change` hook was introduced in Git 2.31/2.32; the `-3` three-way fallback requires Git ≥ 2.32 (gated by `git.DefaultFeatures().CheckVersionAtLeast("2.32")`). The test runtime uses Git 2.55.0, satisfying both. - The hook's exit value is not reflected in the diffpatch HTTP response, so the proof relies on the on-disk marker file and the `hook_ran_as_user` log rather than the API status code (Call #2 returns 201 on the vulnerable build). - The temporary upload repository is cleaned up by Gitea after the operation, so the planted hook file is transient; the durable proof is the marker the hook wrote while it was live. ### Reproduction - Reproduced: 2026-07-29T10:56:28.309Z - Duration: 1806s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00312 # or: pruva-verify CVE-2026-60004 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00312 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00312/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00312 ================================================================================ ## REPRO-2026-00311: xrdp Xvnc backend authentication issue on RHEL 9 -------------------------------------------------------------------------------- Status: published Severity: high Type: security ### Identifiers - REPRO ID: REPRO-2026-00311 - CVE: CVE-2026-55626 (https://nvd.nist.gov/vuln/detail/CVE-2026-55626) ### Package Information - Name: neutrinolabs/xrdp - Ecosystem: github - Affected: GitHub advisory range is xrdp 0.10.3 through 0.10.6 inclusive. The RHEL 9 report reproduced on xrdp-0.10.6-1.el9.x86_64. - Fixed: Unknown - Severity: high - CVSS: Unknown - CWE: Unknown ### Root Cause # Root Cause Analysis: CVE-2026-55626 ## Summary CVE-2026-55626 is a missing-authentication flaw in xrdp's Xvnc-over-UNIX-domain-socket (`Xvnc-UDS`) session backend. xrdp deliberately starts Xvnc with RFB `SecurityTypes None` because access to the intended UNIX socket is controlled by filesystem permissions. Before the fix, however, xrdp did not disable Xvnc's separate TCP RFB listener. Consequently, the same desktop was also reachable on localhost TCP port `5900 + display` without any RFB credentials. A local peer able to reach that loopback listener could connect directly and view or control another user's active desktop, bypassing xrdp's intended per-session authorization boundary. ## Impact - **Affected package/component:** xrdp, specifically `sesman/sesexec/session.c` in the Xvnc-UDS session-start path, together with a TigerVNC-compatible Xvnc backend. - **Affected upstream versions:** xrdp 0.10.3 through 0.10.6. The release-line parent tested here, `ee84a41c7d76f651bea45b89303d56d894a2f057`, is after the `v0.10.6` tag and immediately before the security fix. - **Fixed version:** xrdp 0.10.6.1. - **Risk:** High. A local authenticated or otherwise local network peer can bypass the intended UNIX-socket access control and obtain an unauthenticated RFB session to another user's desktop. Successful access permits desktop confidentiality and integrity compromise and may disrupt the session. - **Scope clarification:** The vulnerable unintended peer is Xvnc's loopback TCP listener. Xvnc's intended UNIX-domain socket remains permission-controlled. Normal Xvnc-over-TCP mode and xorgxrdp are not this bug. ## Impact Parity - **Disclosed/claimed maximum impact:** Authentication/authorization bypass allowing unauthorized viewing or control of active desktop sessions. - **Reproduced impact:** An unauthenticated RFB 3.8 peer connected over real TCP to each vulnerable Xvnc process, selected security type `None` (`1`), received a successful security result, and reached `ServerInit` for the live 320x240 desktop without supplying a username, password, cookie, or other credential. The fixed build refused the same TCP connections. - **Parity:** `full`. - **Not demonstrated:** The proof stops at successful authenticated-session bypass and desktop initialization; it does not transmit framebuffer/input messages, steal user data, execute commands, escalate privileges, or claim Internet-remote reachability. Those stronger actions are unnecessary to establish the disclosed authorization bypass. ## Root Cause For an Xvnc-UDS session, `prepare_xvnc_xserver_params()` constructs the Xvnc command line. The vulnerable code adds: ```text -rfbunixpath -rfbunixmode 432 -SecurityTypes None ``` `432` is decimal notation for mode `0660`. The design assumes UNIX-socket ownership and permissions are the sole authorization mechanism, so disabling in-protocol RFB authentication is intentional for that socket. The mistake is that adding `-rfbunixpath` does not implicitly suppress Xvnc's default TCP listener. The generated command therefore exposes two transports sharing `SecurityTypes None`: 1. the intended permission-controlled UNIX socket; and 2. an unintended loopback TCP socket at `5900 + display`, which has no filesystem authorization boundary. The vulnerable runtime command captured in `bundle/logs/vulnerable-attempt-1-processes.log` lacks a TCP-disable option: ```text Xvnc :10 ... -rfbunixpath /tmp/cve55626-vulnerable/run/xrdp/1000/xrdp_display_10 -rfbunixmode 432 -SecurityTypes None ... ``` The fixed runtime command in `bundle/logs/fixed-attempt-3-processes.log` adds `-rfbport -1`: ```text Xvnc :10 ... -rfbport -1 -rfbunixpath /tmp/cve55626-fixed/run/xrdp/1000/xrdp_display_10 -rfbunixmode 432 -SecurityTypes None ... ``` The one-line upstream fix is commit [`517b8a180d8cbad1b7950ff4f6b31491318f5bb5`](https://github.com/neutrinolabs/xrdp/commit/517b8a180d8cbad1b7950ff4f6b31491318f5bb5) on the v0.10 release line. It inserts `"-rfbport", "-1"` before `-rfbunixpath`, preventing creation of the unintended TCP listener. `bundle/logs/security_patch.diff` captures this exact change. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. It accepts `PRUVA_ROOT` and otherwise derives the bundle root from its own path. 2. The script reads `bundle/project_cache_context.json`, uses the prepared checkout when available, resolves the fixed commit and its exact parent, and verifies that only the fixed side contains the expected patch hunk. 3. It installs its clean-sandbox dependencies, builds and installs both exact xrdp revisions, and uses the real `xrdp-sesman`, `xrdp-sesexec`, `xrdp-sesrun`, and TigerVNC `Xvnc` programs. 4. It creates two isolated vulnerable Xvnc-UDS sessions and two isolated fixed sessions. Each session crosses xrdp's real session-start path before an RFB 3.8 client connects through a localhost TCP socket. 5. The client deliberately supplies no credentials. For vulnerable sessions, the script requires RFB security type `None`, a successful security result, and receipt of `ServerInit`. For fixed sessions, it requires the same TCP connection to fail closed. 6. The script writes `bundle/repro/runtime_manifest.json` on every attempt and exits `0` only when all two vulnerable attempts and both fixed controls satisfy their assertions. Expected terminal result: ```text CONFIRMED: vulnerable xrdp Xvnc-UDS sessions exposed an unauthenticated RFB TCP peer; fixed commit disabled that TCP listener. ``` ## Evidence - `bundle/logs/reproduction_steps.log` — complete latest run, including exact source identities and all four probe outcomes. - `bundle/logs/source_identity.log` — vulnerable commit `ee84a41c7d76f651bea45b89303d56d894a2f057` and fixed commit `517b8a180d8cbad1b7950ff4f6b31491318f5bb5`. - `bundle/logs/security_patch.diff` — one-line `-rfbport -1` patch. - `bundle/logs/vulnerable-attempt-1-rfb.json` and `vulnerable-attempt-2-rfb.json` — each records `credentials_supplied: false`, `connected: true`, `security_types: [1]`, `security_result: 0`, and `server_init_received: true`. - `bundle/logs/fixed-attempt-3-rfb.json` and `fixed-attempt-4-rfb.json` — each records `credentials_supplied: false`, `connected: false`, `server_init_received: false`, and `ConnectionRefusedError`. - `bundle/logs/vulnerable-attempt-{1,2}-processes.log` — live vulnerable Xvnc command lines with `-SecurityTypes None` and no `-rfbport -1`. - `bundle/logs/fixed-attempt-{3,4}-processes.log` — live fixed Xvnc command lines containing `-rfbport -1`. - `bundle/logs/*-session-launch.log` and `bundle/logs/*-sesman.log` — product session-start diagnostics showing the xrdp path was exercised. - `bundle/repro/runtime_manifest.json` — strict runtime manifest with `entrypoint_kind: "tcp_peer"`, all reachability flags true, the runtime stack, and concrete proof paths. - `bundle/logs/repro_evidence_sha256.txt` — SHA-256 inventory for the primary proof artifacts. Latest vulnerable probe excerpt: ```json { "credentials_supplied": false, "connected": true, "none_security_offered": true, "security_types": [1], "security_result": 0, "server_init_received": true, "width": 320, "height": 240 } ``` Latest fixed negative-control excerpt: ```json { "credentials_supplied": false, "connected": false, "server_init_received": false, "error": "ConnectionRefusedError(111, 'Connection refused')" } ``` The current worker is Ubuntu 26.04 rather than RHEL 9, but it runs the affected upstream xrdp code with a real TigerVNC Xvnc implementation supporting the same `-rfbunixpath`, `-SecurityTypes None`, and `-rfbport -1` semantics used by the RHEL 9 deployment. No sanitizer or mocked parser/service was used. ## Recommendations / Next Steps 1. Upgrade to xrdp 0.10.6.1 or later, or backport commit `517b8a180d8cbad1b7950ff4f6b31491318f5bb5`. 2. Ensure every Xvnc-UDS launch explicitly disables TCP RFB with `-rfbport -1`; do not assume `-localhost` or `-nolisten tcp` disables the RFB listener (`-nolisten tcp` concerns the X11 transport). 3. As defense in depth, restrict local access to VNC/RFB ports and audit active Xvnc command lines/listening sockets for `-SecurityTypes None` combined with an enabled TCP RFB port. 4. Add an integration regression test that starts Xvnc-UDS, verifies the UNIX socket exists and remains usable by the authorized consumer, and asserts that `5900 + display` refuses TCP connections. 5. Test both vulnerable-style and fixed-style behavior against the TigerVNC package shipped on supported RHEL 9 systems. ## Additional Notes - **Idempotency:** The final script completed successfully twice consecutively after clean per-attempt process and state cleanup. Each execution itself performs two vulnerable attempts and two fixed controls. - **Runtime boundary:** The attack probe is a real RFB TCP peer, not a direct call to `prepare_xvnc_xserver_params()` or a reimplementation of Xvnc. - **Authentication boundary:** xrdp authenticates/authorizes the session owner before starting the desktop. The bypass is subsequent direct access to that already-running desktop through Xvnc's unintended no-auth TCP listener. - **Attacker locality:** Upstream describes a local attacker and `-localhost` binds the unintended listener to loopback. This confirms authorization bypass at a network-protocol TCP boundary, but does not establish access from an arbitrary remote host without an additional local foothold, tunnel, namespace route, or other localhost reachability mechanism. - **Display allocation:** Display numbers can increase when prior X11 lock files remain; the script discovers the actual live display from the generated Xvnc command and probes its corresponding TCP port rather than relying on display `:10`. ### Reproduction - Reproduced: 2026-07-29T10:06:49.181Z - Duration: 3870s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00311 # or: pruva-verify CVE-2026-55626 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00311 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00311/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00311 ================================================================================ ## REPRO-2026-00310: Flowise arbitrary file access via unvalidated chatflowId/chatId -------------------------------------------------------------------------------- Status: published Severity: critical Type: security ### Identifiers - REPRO ID: REPRO-2026-00310 - CVE: CVE-2025-71334 (https://nvd.nist.gov/vuln/detail/CVE-2025-71334) ### Package Information - Name: FlowiseAI/Flowise - Ecosystem: npm - Affected: GitHub Advisory Database and OSV list flowise >=2.2.8 and <3.0.6; patched version is 3.0.6. - Fixed: Unknown - Severity: critical - CVSS: Unknown - CWE: CWE-73 ### Root Cause # Root Cause Analysis ## Summary Flowise 3.0.5 exposes the public `GET /api/v1/get-upload-file` endpoint without authentication and passes its attacker-controlled `chatId` query parameter into the local-storage path built by `streamStorageFile`. The function validates `chatflowId` but not `chatId`. In its legacy no-organization fallback, `path.join(storageRoot, chatflowId, chatId, filename)` therefore normalizes `../` segments and can resolve outside the configured storage root. A remote unauthenticated request can consequently retrieve a file from the parent of local storage. This run reproduced the issue twice against real Flowise 3.0.5 HTTP servers and showed that the identical requests are rejected twice by Flowise 3.0.6. ## Impact - **Affected package/component:** `flowise` / `flowise-components`, specifically the public `get-upload-file` handler and `streamStorageFile` local-storage fallback. - **Affected version reproduced:** Flowise `3.0.5` (source commit `ba6a602cbe87d9f55c9ee6aebb6407ec2f2066b5`; exact official linux/amd64 image manifest `sha256:30d4fdf8b9e215abff31a67ab104a9750ca25354fe98fe97a3481bbca0352098`). The ticket describes Flowise versions before `3.0.6` as affected. - **Fixed version tested:** Flowise `3.0.6` (source commit `89a0f23fe5e9c0b1ee85ee1175032c6b9e5ac9c1`; exact official linux/amd64 image manifest `sha256:86b83c5f55cd7989453789a39c568d08885be50e74faf9abd5e238269bcfe489`). - **Risk:** High/critical confidentiality risk. An unauthenticated network client with a valid chatflow UUID can read files reachable through traversal from the configured local storage hierarchy. In the default layout, this can expose application state such as the SQLite database and its sensitive records. The vulnerable fallback also copies the source into storage and unlinks the original, creating a data-tampering/availability side effect. ## Impact Parity - **Disclosed/claimed maximum impact:** Pre-auth arbitrary file read/write, information disclosure, and data tampering through Flowise file-storage APIs. - **Reproduced impact:** Pre-auth arbitrary file read through the real HTTP endpoint. A unique secret was created outside `BLOB_STORAGE_PATH`; an HTTP request sent without cookies, `Authorization`, API key, or `x-request-from` returned that exact secret with status 200. The vulnerable fallback then moved the source file into storage, also demonstrating an unauthorized filesystem mutation. - **Parity:** `full` for the canonical contract's `info_leak` impact and the unauthenticated API surface. - **Not demonstrated:** A general attacker-controlled arbitrary-file-write primitive was not needed for the canonical claim and was not claimed as independently proven here. Code execution was neither required nor attempted. ## Root Cause The endpoint is included in `WHITELIST_URLS`, so Flowise's global API middleware allows requests to `/api/v1/get-upload-file` without authentication. The controller reads `chatflowId`, `chatId`, and `fileName` directly from query parameters, resolves the organization from the referenced chatflow, and calls: ```ts streamStorageFile(chatflowId, chatId, fileName, orgId) ``` In Flowise 3.0.5, `streamStorageFile` validates that `chatflowId` is a UUID and rejects traversal only in `chatflowId`. It does not apply `isPathTraversal` to `chatId`. The primary local path is checked, but when it does not exist the migration fallback constructs a second path without the organization prefix: ```ts const fallbackPath = path.join(getStoragePath(), chatflowId, chatId, sanitizedFilename) ``` Because Node's `path.join` normalizes traversal segments, a value such as `chatId=../..` transforms `storageRoot//../../outside-secret.txt` into a path above `storageRoot`. Critically, this fallback path is not checked with the primary path's absolute/root-containment checks before `existsSync`, `copyFileSync`, `unlinkSync`, and `createReadStream` are used. Filename sanitization cannot constrain traversal supplied through the separate `chatId` component. Flowise 3.0.6 fixes the reproduced mechanism by extending the early guard to both path components: ```ts if (isPathTraversal(chatflowId) || isPathTraversal(chatId)) { throw new Error('Invalid path characters detected in chatflowId or chatId') } ``` The version-paired source diff is captured in `bundle/repro/root_cause_source.txt`. The release change is present between commits `ba6a602cbe87d9f55c9ee6aebb6407ec2f2066b5` and `89a0f23fe5e9c0b1ee85ee1175032c6b9e5ac9c1`. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. It honors `PRUVA_ROOT` and the prepared project cache described by `bundle/project_cache_context.json`. 2. The script verifies the exact Flowise Git tags/commits and the presence/absence of the fixing hunk. It then downloads and digest-pins the official linux/amd64 Flowise 3.0.5 and 3.0.6 container filesystems, and runs their bundled Node runtimes and real Flowise CLI/server binaries directly. 3. For each of two isolated attempts per version, it starts Flowise with SQLite and local storage, waits for `/api/v1/ping`, performs administrative setup to create a valid chatflow, places a unique secret just outside `BLOB_STORAGE_PATH`, and sends the exploit request with no authentication material: ```text GET /api/v1/get-upload-file?chatflowId=&chatId=../..&fileName=outside-secret.txt ``` 4. Expected evidence: - Both 3.0.5 attempts return HTTP 200 and the exact unique outside secret, followed by `VULNERABLE_UNAUTHENTICATED_READ_CONFIRMED`. - Both 3.0.6 attempts return HTTP 500 with `Invalid path characters detected in chatflowId or chatId`; the source file remains unchanged, followed by `FIXED_REJECTION_CONFIRMED`. - The script exits 0 only after all four checks pass and prints `REPRODUCTION_CONFIRMED`. ## Evidence - `bundle/logs/reproduction_steps.log` — complete image acquisition, server startup, request/response, and four-attempt verdict transcript. - `bundle/logs/vulnerable_attempt1_response.txt` and `bundle/logs/vulnerable_attempt2_response.txt` — unique bytes disclosed by the unauthenticated endpoint. - `bundle/logs/fixed_attempt1_response.txt` and `bundle/logs/fixed_attempt2_response.txt` — fixed-build rejection JSON. - `bundle/logs/vulnerable_attempt1_service.log`, `bundle/logs/vulnerable_attempt2_service.log`, `bundle/logs/fixed_attempt1_service.log`, and `bundle/logs/fixed_attempt2_service.log` — real Flowise initialization and listening-server evidence. - `bundle/logs/flowise_3.0.5_image_manifest.json` and `bundle/logs/flowise_3.0.6_image_manifest.json` — exact official linux/amd64 OCI manifest and layer identities. - `bundle/repro/source_identity.log` — source tags, commits, and image digests. - `bundle/repro/runtime_manifest.json` — strict runtime manifest with `entrypoint_kind=endpoint` and service, healthcheck, and target-path flags set to true. - `bundle/repro/root_cause_source.txt` — bounded vulnerable code, fixed diff, whitelist, and controller source evidence. Representative successful-run excerpts: ```text unauthenticated_status=200 PRUVA_VULNERABLE_1_ VULNERABLE_UNAUTHENTICATED_READ_CONFIRMED ``` ```text unauthenticated_status=500 {"statusCode":500,"success":false,"message":"Invalid path characters detected in chatflowId or chatId","stack":{}} FIXED_REJECTION_CONFIRMED ``` ## Recommendations / Next Steps - Upgrade self-managed Flowise installations to `3.0.6` or later. - Validate every attacker-controlled path segment (`chatflowId`, `chatId`, organization identifiers, and filenames) before storage-provider use. Prefer strict expected-format validation, such as UUID validation where applicable. - After constructing both primary and fallback paths, resolve them with `path.resolve` and enforce containment using a separator-aware relative-path check; a simple string prefix is insufficient. - Remove or tightly constrain legacy fallback/migration behavior on unauthenticated routes. Filesystem moves should not occur as a side effect of a public download request. - Add integration tests over the real unauthenticated HTTP boundary for encoded and unencoded traversal forms, both download endpoints, all storage providers, and fixed-version fail-closed behavior. - Return a client error (for example HTTP 400) rather than HTTP 500 for invalid path input to reduce unnecessary internal-error behavior. ## Additional Notes - **Idempotency:** The final script performs two isolated vulnerable and two isolated fixed attempts in one run, and it is being executed twice consecutively. Per-attempt directories, databases, accounts, chatflows, ports, and secrets are recreated; each run uses a new random token. - **Authentication boundary:** Registration/login and chatflow creation are setup actions only. The exploit request itself is emitted by a fresh `curl` invocation without a cookie jar or any authentication-related header. - **Execution mode:** No sanitizer, direct parser harness, mock server, or reimplemented storage function is used. The script invokes the real released Flowise CLI and HTTP server from digest-pinned official product images. - **Operational detail:** The official image filesystems are streamed and extracted rather than imported into the rootless Docker daemon because that daemon's private layer store was too small for these multi-gigabyte images. This does not change product bytes or runtime behavior: each image's own bundled musl loader, Node executable, Flowise package, dependencies, and CLI are executed. - **Known precondition:** The read handler requires a valid chatflow UUID. The script creates one through the normal authenticated product API before testing the separate public download boundary. ### Reproduction - Reproduced: 2026-07-28T15:45:55.651Z - Duration: 4017s - Confidence: high ### Quick Verify ```bash pruva-verify REPRO-2026-00310 # or: pruva-verify CVE-2025-71334 ``` ### Links - Detail page: https://www.pruva.dev/reproductions/REPRO-2026-00310 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00310/artifacts/bundle/repro/reproduction_steps.sh - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00310 ================================================================================ ## REPRO-2026-00309: PipeWire sandbox escape via malicious library loading in PulseAudio compatibility layer -------------------------------------------------------------------------------- Status: published Severity: high Type: security ### Identifiers - REPRO ID: REPRO-2026-00309 - CVE: CVE-2026-5674 (https://nvd.nist.gov/vuln/detail/CVE-2026-5674) ### Package Information - Name: pipewire (pipewire-pulse daemon) - Ecosystem: Unknown - Affected: Unknown - Fixed: Unknown - Severity: high - CVSS: Unknown - CWE: Unknown ### Root Cause # Root Cause Analysis — CVE-2026-5674 (PipeWire PulseAudio-compatibility sandbox escape) ## Summary PipeWire's PulseAudio compatibility daemon (`pipewire-pulse`) implements the PulseAudio native protocol `LOAD_MODULE` command. The request handler only checks the `pulse.allow-module-loading` server property (default: `true`) and then loads any of the built-in PulseAudio compatibility modules with fully attacker-controlled arguments. One of those modules, `module-ladspa-sink`, forwards the attacker-supplied `plugin=` argument to PipeWire's filter-chain LADSPA loader, which calls `dlopen()` on the value verbatim when it is an absolute path. Because Flatpak-style sandboxes deliberately expose the PulseAudio unix socket to sandboxed applications, an attacker confined in a bubblewrap/Flatpak sandbox can make the out-of-sandbox `pipewire-pulse` daemon `dlopen()` an attacker-controlled shared library, executing its ELF constructors in the daemon process and thereby escaping the sandbox. ## Impact - Package/component affected: `pipewire` / `pipewire-pulse` (`libpipewire-module-protocol-pulse`, pulse `module-ladspa-sink`/`module-ladspa-source`, SPA filter-graph LADSPA plugin). - Affected versions: verified on PipeWire 1.6.2 (Ubuntu 26.04, `1.6.2-1ubuntu1.1`). The Debian security tracker lists every release as vulnerable (bullseye 0.3.19 through sid 1.6.8); no upstream fix exists at the time of this run. - Risk level and consequences: Important (CVSS 8.8 per Amazon ALAS). Any sandboxed application with access to the PulseAudio socket (default for audio-playing Flatpaks) can execute arbitrary code in the user's unsandboxed `pipewire-pulse` session daemon, i.e. a full sandbox escape with the daemon's privileges. ## Impact Parity - Disclosed/claimed maximum impact: sandbox escape — arbitrary code execution outside the sandbox (bubblewrap/Flatpak-style namespace sandbox) via the PulseAudio compatibility layer. - Reproduced impact from this run: attacker-controlled code executed in the host-side `pipewire-pulse` daemon from a client confined in a real bubblewrap sandbox with separate mount and user namespaces. Proof: the loaded library's constructor wrote fresh markers (`CVE-2026-5674-PWNED pid= uid=1000 comm=pipewire-pulse`) into a host-only oracle directory that the sandboxed client could neither see nor write. - Parity: `full`. - Not demonstrated: nothing material — a constructor can contain arbitrary code; the marker write is representative attacker-controlled code execution in the daemon. ## Root Cause 1. `src/modules/module-protocol-pulse/pulse-server.c` — `do_load_module()` (line 5051) handles `COMMAND_LOAD_MODULE` from the PulseAudio native protocol. Its only guard is `if (!impl->defs.allow_module_loading) return -EACCES;`. The property defaults to true (`/usr/share/pipewire/pipewire-pulse.conf`: `#pulse.allow-module-loading = true`). 2. `src/modules/module-protocol-pulse/modules/module-ladspa-sink.c` — the registered pulse module `module-ladspa-sink` accepts `plugin=` and `label=` arguments and builds a filter.graph node `{ type = ladspa plugin = "" label = "