# Variant Root Cause Analysis — CVE-2026-59092

## Summary

The official fix for CVE-2026-59092 (commit `a46979cdd4082217081ee99b931ddc53d038e47a`, PR #7214) correctly isolated the metrics, WebDAV, and sync-cluster HTTP servers from `http.DefaultServeMux` — closing the *remote* pprof exposure on operator-bindable ports. However, the fix did **not** touch the **debug agent** started in `cmd/main.go:336` (`http.ListenAndServe(debugAgent, nil)`), nor its twin in the Java SDK (`sdk/java/libjfs/main.go:573`). Because `_ "net/http/pprof"` is imported, both debug agents still serve the shared `DefaultServeMux` and still expose `/debug/pprof/cmdline`, which leaks the full process command line — including the metadata-engine URL with database credentials. This is a **distinct variant of the same root cause** (DefaultServeMux + pprof import + `nil` handler) via a **different entry point** (the debug agent port `127.0.0.1:6060` instead of the metrics port) that **the fix did not cover**. It is confirmed to reproduce on the **fixed** binary. Because the debug agent is hardcoded to `127.0.0.1` (no flag rebinds it to `0.0.0.0`), this is **not a remote bypass** of the original CVE; it is a **localhost-only residual / fix-coverage gap** reachable by a co-located local user or an SSRF from a co-located service, and disableable via `--no-agent`.

## Fix Coverage / Assumptions

- **Invariant the fix relies on**: "Every remotely-reachable JuiceFS HTTP server that previously served `DefaultServeMux` is one of `exposeMetrics` (cmd/mount.go), `StartHTTPServer` (pkg/fs/http.go), or `startManager` (pkg/sync/cluster.go)."
- **Code paths explicitly covered**: the metrics port (mount/gateway/sync/mdtest), the WebDAV port, and the sync-cluster manager port. Each now uses a dedicated `http.NewServeMux()` passed as the handler instead of `nil`.
- **What the fix does NOT cover**: the `nil`-handler **debug agent** in `cmd/main.go:336` (started for every subcommand unless `--no-agent`) and the **Java SDK debug agent** in `sdk/java/libjfs/main.go:573`. `git show <fix> -- cmd/main.go` is an empty diff, proving the debug agent was untouched. Both still import `_ "net/http/pprof"` and still serve `DefaultServeMux`.

## Variant / Alternate Trigger

- **Entry point**: an unauthenticated HTTP `GET` to `/debug/pprof/cmdline` on the **debug agent** port `127.0.0.1:6060` (first free port in `6060..6099`) of a running JuiceFS process — *not* the metrics port used by the original CVE.
- **Code path**: `cmd/main.go:332-337` → `http.ListenAndServe("127.0.0.1:6060", nil)` → `DefaultServeMux` → `net/http/pprof` `/debug/pprof/cmdline` handler → `runtime` process command line (which contains the `redis://:s3cr3tPass@127.0.0.1:6379/1` metadata URL passed on the `juicefs gateway`/`mount` command line).
- **Secondary anchor (same root cause, same sink)**: `sdk/java/libjfs/main.go:573` — `http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil)`, gated by `jConf.Debug || JUICEFS_DEBUG`.
- **Why it bypasses the fix's coverage**: the fix only converted the three remote-facing servers to dedicated muxes; the debug agent was left on `DefaultServeMux`, so the identical pprof sink is still reachable on the fixed commit.

## Impact

- **Package/component affected**: `cmd/main.go` (debug agent, `debugAgentOnce` goroutine), and `sdk/java/libjfs/main.go` (Java SDK debug agent). Sink: `net/http/pprof` `/debug/pprof/cmdline` (and sibling handlers `heap`, `goroutine`, `allocs`, `threadcreate`, `block`, `profile`).
- **Affected versions (as tested)**: confirmed present on the **fixed** commit `a46979cdd4082217081ee99b931ddc53d038e47a` (and on the vulnerable parent `f60a90fc` — behavior is unchanged by the fix). Affects all JuiceFS ≤ 1.3.1 *and* the fixed commit, for any process started without `--no-agent`.
- **Risk level**: **Medium (local)** — not a remote bypass. On a shared/multi-tenant host, a co-located unprivileged user or an SSRF from a co-located web service can read the operator's metadata-engine credentials from a fixed JuiceFS process; the other pprof handlers leak internal runtime state and `profile` enables a local CPU-DoS. The original CVE's **remote** auth-bypass on the metrics port is **not** reproduced (metrics port returns 404 on the fixed binary).

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE)**: unauthenticated *remote* authentication bypass and disclosure of metadata-engine connection strings with DB credentials via `/debug/pprof/cmdline`; internal-state leakage and DoS via other pprof handlers.
- **Reproduced impact from this variant run** (on the **fixed** binary):
  - `/debug/pprof/cmdline` on `127.0.0.1:6060` → **HTTP 200**, response body contains `redis://:s3cr3tPass@127.0.0.1:6379/1` (credential leak confirmed).
  - Other pprof handlers on the debug agent return **HTTP 200** (`heap`, `goroutine`, `allocs`, `threadcreate`, `block`; `/debug/pprof/` → 301; `profile` blocks for its profiling duration = DoS surface).
  - Metrics port `/debug/pprof/cmdline` → **HTTP 404** (fix holds on the remote surface); `/metrics` → **HTTP 200** (no regression).
  - Debug agent reachable from a non-loopback host IP (`172.20.0.11:6060`) → **connection refused** (`loopback_only`).
  - With `--no-agent`, the debug agent is **not** listening (mitigation works).
- **Parity**: **partial** — the *same sink and the same credential-leak impact* are reproduced, but on a **localhost-only** surface, not the **remote** surface of the parent CVE. Remote auth-bypass is **not** reproduced (the fix closes it).
- **Not demonstrated**: remote exploitation; cross-host credential theft. The demonstrated primitive is local/loopback credential disclosure on the fixed version.

## Root Cause

The root cause is identical to the parent CVE: a Go HTTP server is started with a `nil` handler (which defaults to the process-wide `http.DefaultServeMux`) while `_ "net/http/pprof"` is imported, so the pprof handlers registered on `DefaultServeMux` become reachable without authentication. The parent CVE's instance was the metrics port (`http.Serve(ln, nil)` in `exposeMetrics`); this variant's instance is the debug agent (`http.ListenAndServe(debugAgent, nil)` in `cmd/main.go:336`). The fix eliminated the `nil`-handler usage in the three remote-facing servers but left the debug agent's `nil`-handler usage intact, so the same underlying bug is still reachable via the debug agent entry point on the fixed commit.

- **Fix commit**: `a46979cdd4082217081ee99b931ddc53d038e47a` (PR #7214).

## Reproduction Steps

1. **Reference**: `bundle/vuln_variant/reproduction_steps.sh`
2. **What the script does** (idempotent, tests both vulnerable and fixed binaries side by side):
   - Reuses the prebuilt `juicefs-vuln` (commit `f60a90fc`) and `juicefs-fixed` (commit `a46979cd`) binaries and a password-protected Redis (`s3cr3tPass`) from the project cache.
   - Formats a JuiceFS volume with `redis://:s3cr3tPass@127.0.0.1:6379/1`.
   - **TEST 1 (vulnerable)**: starts `juicefs-vuln gateway` (debug agent enabled) → checks metrics-port pprof (baseline, expect 200) and scans `127.0.0.1:6060..6099` for the debug agent → curls `/debug/pprof/cmdline` and checks for `s3cr3tPass`.
   - **TEST 2 (fixed)**: starts `juicefs-fixed gateway` (debug agent enabled) → checks metrics-port pprof (expect 404) and `/metrics` (expect 200) → finds the debug agent on `127.0.0.1:6060` → curls `/debug/pprof/cmdline` (the variant) and checks for `s3cr3tPass` → tests reachability from a non-loopback IP (expect refused) → enumerates other pprof handlers.
   - **TEST 3 (mitigation)**: starts `juicefs-fixed gateway --no-agent` → confirms the debug agent is no longer listening.
   - Writes `bundle/vuln_variant/variant_runtime_result.json` and exits **0** if the fixed debug agent leaks credentials (variant confirmed on fixed version), else **1**.
3. **Expected evidence of reproduction**: fixed debug agent `/debug/pprof/cmdline` → HTTP 200 with body containing `redis://:s3cr3tPass@127.0.0.1:6379/1`; fixed metrics port pprof → 404; non-loopback → refused; `--no-agent` → agent disabled.

## Evidence

- **Run logs**: `bundle/logs/vuln_variant/variant-run-{4,5,6,7}.log` (runs 6 & 7 are the verified idempotent pair; both exit 0 with identical results).
- **Runtime evidence**: `bundle/vuln_variant/variant_runtime_result.json` — `variant_on_fixed=true`, `fixed_debug_agent_leak=true`, `fixed_debug_port=6060`, `fixed_metrics_pprof=404`, `fixed_metrics_ok=200`, `fixed_debug_reachability=loopback_only`, `fixed_noagent_result=disabled`.
- **Source identity**: `bundle/logs/vuln_variant/fixed_version.txt` — `fixed_git_rev_parse=a46979cdd4082217081ee99b931ddc53d038e47a`.
- **Captured responses** (raw, NUL→newline):
  - `bundle/logs/vuln_variant/fixed-debugagent-cmdline.txt` — fixed debug agent `/debug/pprof/cmdline` body:
    ```
    /data/pruva/project-cache/.../juicefs-fixed
    gateway
    redis://:s3cr3tPass@127.0.0.1:6379/1   <-- credential leaked on FIXED binary
    localhost:9101
    --metrics
    0.0.0.0:9578
    --no-banner
    ```
  - `bundle/logs/vuln_variant/vuln-debugagent-cmdline.txt` — vulnerable debug agent (identical leak).
- **Gateway logs**: `bundle/logs/vuln_variant/gateway-{vuln,fixed,fixed-noagent}.log`.
- **Endpoint enumeration (fixed debug agent, run #6)**:
  ```
  /debug/pprof/         : HTTP 301
  /debug/pprof/heap     : HTTP 200
  /debug/pprof/goroutine: HTTP 200
  /debug/pprof/profile  : HTTP 000 (blocks for profiling duration -> DoS surface)
  /debug/pprof/allocs   : HTTP 200
  /debug/pprof/threadcreate: HTTP 200
  /debug/pprof/block    : HTTP 200
  ```
- **Environment**: Go 1.25.0 linux/amd64, Redis 8.0.5, x86_64; vulnerable commit `f60a90fc0ad52d2bb1f44f38a04d55044fc91d50`; fixed commit `a46979cdd4082217081ee99b931ddc53d038e47a` (verified `git rev-parse HEAD`).

## Recommendations / Next Steps

1. **Extend the fix to the debug agent** (`cmd/main.go:332-337`): serve a dedicated mux (or register pprof on a separate mux that the debug agent's listener serves) so `/debug/pprof/*` is not exposed via `DefaultServeMux` even on localhost. Apply the same to `sdk/java/libjfs/main.go:569-574`.
2. **Defense in depth**: keep the `127.0.0.1` binding and document that `--no-agent` (or `JUICEFS_DEBUG` unset for the Java SDK) is **required on shared/multi-tenant hosts**, since a co-located user or SSRF can otherwise extract metadata credentials from a fixed deployment.
3. **Optionally** add a regression test mirroring `cmd/mount_test.go`/`pkg/sync/cluster_test.go` that asserts the debug agent does not leak `os.Args` via `/debug/pprof/cmdline` when pprof is isolated.
4. **Do not** treat this as a re-opening of the remote metrics-port surface — that is correctly closed; this is a local-hardening gap in the same root-cause family.

## Additional Notes

- **Idempotency**: the script was run multiple times; runs #6 and #7 (the verified pair) both completed with exit 0 and identical summaries, with no lingering processes. Earlier port-race failures were fixed by adding a port-free wait + global cleanup; cleanup is targeted to the exact built binary paths to avoid self-kill.
- **Trust boundary**: the original CVE is *remote* (operator binds metrics to `0.0.0.0`); this variant is *localhost* (debug agent hardcoded to `127.0.0.1`, no remote-bind flag). It crosses a **local** user-to-user/process-to-process boundary, not the parent's remote boundary — hence "partial" parity and "local" risk, not a remote bypass.
- **Bounded search**: a full scan of the fixed commit found exactly two remaining `nil`-handler `DefaultServeMux` servers (the two debug agents). All other HTTP servers (metrics/WebDAV/sync) were converted by the fix; the gateway's S3 port is served by minio's own router, not `DefaultServeMux`. No additional materially distinct remote entry points exist.
