# Variant Root Cause Analysis — CVE-2026-58466 (AutoBangumi default admin credentials)

## Summary

The parent RCA reproduced the hard-coded default-credential takeover
(`admin`/`adminadmin`) on AutoBangumi **3.2.6** via `POST /api/v1/auth/login`
and noted that the advisory-referenced "fix" (commit `487bdfec`, version 3.2.8)
is SSRF hardening of the pre-auth `/setup/test-*` endpoints — it does **not**
touch `add_default_user()` or the login endpoint, and the 3.2.8 negative control
still accepted the default credentials.

This variant stage confirms **two materially distinct bypasses/variants on the
FIXED and LATEST official Docker images** (`ghcr.io/estrellaxd/auto_bangumi:latest`
= the 3.2.8 build, and `:3.3.0-beta.2`):

- **Variant 1 — BYPASS (same root cause, fixed version):** The default
  credentials `admin`/`adminadmin` still authenticate via
  `POST /api/v1/auth/login` on the patched `:latest` (=3.2.8) **and** on
  `:3.3.0-beta.2`, granting a real admin JWT and full admin API access
  (`/api/v1/rss` → 200). The seeding code and login handler are byte-identical
  (modulo an async DB refactor) from 3.2.6 through HEAD, so the referenced patch
  is **ineffective** for this CVE — the original exploit is itself a bypass.
- **Variant 2 — ALTERNATE TRIGGER (different entry point, fixed version):** The
  pre-auth `POST /api/v1/setup/complete` endpoint, reachable on any fresh
  instance (`GET /api/v1/setup/status` → `{"need_setup":true}`), calls
  `db.user.update_user("admin", UserUpdate(username=…, password=…))` and
  **resets the auto-seeded admin account's username/password to attacker-chosen
  values without ever needing the default credentials.** The attacker then logs
  in with their own credentials and obtains full admin API access. This is a
  different entry point (`/api/v1/setup/complete` vs `/api/v1/auth/login`),
  the same fresh-instance admin-takeover impact, and it also reproduces on the
  fixed/latest version — meaning a fix that only gates `/auth/login` would
  **not** stop it.

A third candidate, the `DEV_AUTH_BYPASS` path (`get_current_user` returns
`"dev_user"` when `module.__version__` is unimportable → `VERSION == "DEV_VERSION"`),
was **explicitly ruled out** for the official images: they ship a real
`VERSION` (3.2.8 / 3.3.0-beta.2), and a no-auth `GET /api/v1/rss` returns
`401`. It is documented as a latent source-install concern, not claimed as a
production variant.

## Fix Coverage / Assumptions

- **Invariant the original fix relies on:** that the only pre-auth
  security-relevant surface in 3.2.8 is the SSRF reach of
  `/setup/test-downloader` / `/setup/test-rss` and the raw-error echo from
  those probes.
- **Code paths it explicitly covers:** `backend/src/module/api/setup.py`
  (`_validate_scheme`, `_validate_url`, server-side-only error logging) and the
  qBittorrent client login/leak fixes. See `patch_analysis.md`.
- **What the fix does NOT cover:** `add_default_user()` seeding, the
  `/api/v1/auth/login` endpoint, the `/api/v1/setup/complete` admin-account
  reset, and `get_current_user`/`check_login_ip`. `git diff 3.2.6..HEAD` shows
  **no change** to `user.py`/`auth.py`/`security/api.py` root-cause logic; the
  `3.3.0-beta.2` changes are an async-DB refactor + cookie `samesite="strict"` +
  `logout` GET→POST, none of which add a setup gate or remove the default
  credentials.

## Variant / Alternate Trigger

### Variant 1 — default-credential login bypass (same root cause)

- **Entry point:** `POST /api/v1/auth/login` (FastAPI OAuth2 password form,
  port 7892). Dependency: `Depends(check_login_ip)` only — `login_whitelist`
  defaults to `[]` (allow all). **No setup-completion gate.**
- **Code path:** startup `start_up()`/`first_run()`
  (`module/update/startup.py`) → `UserDatabase.add_default_user()`
  (`module/database/user.py`) seeds `User(username="admin",
  password=get_password_hash("adminadmin"))` on an empty users table →
  attacker POSTs `username=admin&password=adminadmin` → `auth_user()` →
  `verify_password(adminadmin)` succeeds → `_issue_token` sets `token` cookie +
  returns `access_token` (JWT `sub=admin`) → cookie grants admin API access.
- **Result on fixed/latest:** `:latest` (3.2.8) and `:3.3.0-beta.2` both return
  `HTTP 200` + admin JWT; `/api/v1/rss` → `200 []`.

### Variant 2 — `/api/v1/setup/complete` pre-auth takeover (alternate trigger)

- **Entry point:** `POST /api/v1/setup/complete` (JSON body, **no auth
  dependency**). Guard is only `_require_setup_needed()`, which passes on a
  fresh instance because `config/.setup_complete` does not exist **and**
  `settings.dict() == Config().dict()` (fresh defaults).
- **Code path:** `_require_setup_needed()` passes → handler step 1 calls
  `db.user.update_user("admin", UserUpdate(username=req.username,
  password=req.password))` (`module/api/setup.py:302`) which **commits** the
  rename + new password for the auto-seeded `admin` account **before** any
  config save → step 4 creates the `.setup_complete` sentinel. The attacker
  then calls `POST /api/v1/auth/login` with their chosen creds → admin JWT
  (`sub=pwned`) → `GET /api/v1/rss` → `200`.
- **Notable:** the user-account reset commits in step 1 *before* the config
  save in step 2, so even a "failed" `/setup/complete` (e.g. invalid
  downloader) would still have changed the admin credentials; and because
  `/auth/login` has no setup gate, the new creds are immediately usable.
- **Result on fixed/latest:** on `:latest` (3.2.8), `GET /api/v1/setup/status`
  → `{"need_setup":true,"version":"3.2.8"}`; `POST /api/v1/setup/complete` →
  `200 "Setup completed successfully."`; login `pwned`/`pwnedpw1` → `200` +
  JWT `sub=pwned`; `/api/v1/rss` → `200`; afterwards `admin`/`adminadmin` →
  `401 "User not found"` (account taken over).

### Ruled-out candidate — DEV_AUTH_BYPASS

- `module/security/api.py`: `DEV_AUTH_BYPASS = VERSION == "DEV_VERSION"`; if
  active, `get_current_user` returns `"dev_user"` for **all** protected
  endpoints with zero credentials (a distinct no-creds auth bypass).
- **Ruled out for official images:** `module.__version__.VERSION` resolves to
  `3.2.8` / `3.3.0-beta.2` (real version shipped); no-auth `GET /api/v1/rss` →
  `401 Unauthorized` on every tested image. Latent only for source installs
  missing `module/__version__`; **not** claimed as a production variant.

## Impact

- **Package/component affected:** AutoBangumi backend —
  `module/database/user.py` (`add_default_user`), `module/api/auth.py`
  (`login`), `module/api/setup.py` (`complete_setup`), invoked from
  `module/update/startup.py`.
- **Affected versions (as tested):**
  - `ghcr.io/estrellaxd/auto_bangumi:3.2.6` (reported VERSION 3.2.6) —
    vulnerable baseline.
  - `ghcr.io/estrellaxd/auto_bangumi:latest` (reported VERSION **3.2.8**, the
    claimed patch) — **both variants reproduce**.
  - `ghcr.io/estrellaxd/auto_bangumi:3.3.0-beta.2` (reported VERSION
    3.3.0-beta.2, latest beta) — Variant 1 reproduces.
- **Risk level:** Critical. CWE-1392 (Use of Default Credentials) for Variant 1;
  CWE-306 (Missing Authentication for Critical Function / first-run takeover)
  for Variant 2. Remote, unauthenticated, low complexity, full admin takeover.
- **Consequences:** Complete administrative takeover of a fresh AutoBangumi
  instance — RSS feed config, downloader config (incl. credentials), server
  logs, and all authenticated API endpoints.

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** unauthenticated attacker
  authenticates as administrator using publicly known default credentials and
  gains full administrative access.
- **Reproduced impact from this variant run:** **full parity**.
  - Variant 1: default `admin`/`adminadmin` → `200` + admin JWT (`sub=admin`)
    → `/api/v1/rss` `200 []` on the patched `:latest` and `:3.3.0-beta.2`.
  - Variant 2: pre-auth `/setup/complete` → attacker creds `pwned`/`pwnedpw1`
    → `200` + admin JWT (`sub=pwned`) → `/api/v1/rss` `200 []` on `:latest`.
- **Parity:** `full`.
- **Not demonstrated:** N/A — the claimed admin-takeover impact was reproduced
  end-to-end through the real remote API on the fixed/latest product. No
  memory-corruption / RCE primitive is in scope for this CVE.

## Root Cause

The same underlying bug — *a fresh AutoBangumi instance auto-seeds a known
admin account and exposes unauthenticated paths to authenticate as / reset that
account before any operator configuration* — is still reachable because the
referenced fix (`487bdfec` / 3.2.8) is SSRF hardening of `/setup/test-*` and
does not modify the seeding, the login endpoint, or the setup-complete handler.
`add_default_user()` is byte-identical (modulo async refactor) from 3.2.6
through `3.3.0-beta.2` (`c8f402fd`) and `main` HEAD (`b090ec7b`); the login
handler gained no setup gate. Variant 2 additionally exploits that
`/api/v1/setup/complete` is pre-auth on a fresh instance and commits an
admin-account password reset (relying on the auto-seeded `admin` row existing)
before any gated configuration step.

- Fix commit referenced by advisory: `487bdfec545e805ae416e6ddf28651bd274d6a73`
  ("fix(api): harden pre-auth setup endpoints (#1041, #1044)").
- Tested fixed/latest commits: `3.2.8` tag `265b449fad6d753f061a09aaa03fcd3eb739a266`
  (image `:latest`), `3.3.0-beta.2` tag `c8f402fd687c443d91e6c6dc3474032b9a9182eb`
  (image `:3.3.0-beta.2`), `main` HEAD `b090ec7b02fd91a10bf45c7702ad392ae3ad65ef`.

## Reproduction Steps

1. The self-contained script is
   **`bundle/vuln_variant/reproduction_steps.sh`**.
2. What it does (idempotent, cleans up all containers/volumes on entry and
   exit):
   - Pulls the official images `:3.2.6`, `:latest` (=3.2.8), `:3.3.0-beta.2`.
   - For each image, starts a fresh instance with an empty Docker volume,
     waits for `Application startup complete`, captures the startup log
     (`[Database] Created default admin user`) and the image's
     `module.__version__.VERSION`.
   - **Variant 1:** `POST /api/v1/auth/login` with `admin`/`adminadmin`; on
     `200` + JWT, reuses the `token` cookie to call `GET /api/v1/rss`.
   - **DEV rule-out:** no-auth `GET /api/v1/rss` (expect `401`).
   - **Variant 2:** on a fresh `:latest` instance, `GET /api/v1/setup/status`,
     then `POST /api/v1/setup/complete` with attacker creds
     `pwned`/`pwnedpw1`, then `POST /api/v1/auth/login` with those creds, then
     `GET /api/v1/rss` with the resulting JWT, then `POST /api/v1/auth/login`
     with `admin`/`adminadmin` (expect `401` post-takeover).
   - Writes HTTP artifacts to `bundle/vuln_variant/artifacts/`, startup logs
     to `bundle/logs/vuln_variant/`, and the runtime manifest to
     `bundle/vuln_variant/runtime_manifest.json`.
3. Expected evidence of reproduction (all observed):
   - Startup line `[Database] Created default admin user` on 3.2.6, :latest
     (3.2.8), and :3.3.0-beta.2.
   - Variant 1 on :latest: login `200` + `set-cookie: token=eyJ…sub=admin…`;
     `/api/v1/rss` `200 []`.
   - Variant 1 on :3.3.0-beta.2: login `200` + admin JWT.
   - Variant 2 on :latest: `/setup/status` `{"need_setup":true,"version":"3.2.8"}`;
     `/setup/complete` `200`; login `pwned`/`pwnedpw1` `200` + JWT `sub=pwned`;
     `/api/v1/rss` `200`; `admin`/`adminadmin` `401 "User not found"`.
   - DEV rule-out: no-auth `/api/v1/rss` `401` on every image.

## Evidence

- **Run log:** `bundle/logs/vuln_variant/run.log`
- **Startup logs:** `bundle/logs/vuln_variant/{vuln,latest,beta,setup}-startup.log`
- **Fixed/latest version record:** `bundle/logs/vuln_variant/fixed_version.txt`,
  `latest_version.txt`
- **HTTP artifacts:** `bundle/vuln_variant/artifacts/{vuln,latest,beta}-{login,rss,noauth-rss}.json`,
  `setup-{status,complete,login,rss,oldlogin}.json`
- **Runtime manifest:** `bundle/vuln_variant/runtime_manifest.json`
- **Source identity:** `bundle/vuln_variant/source_identity.json`

Key excerpts (from the second idempotent run, `:latest` = 3.2.8):

Variant 1 — default creds on the patched version
(`artifacts/latest-login.json`):
```json
{ "method": "POST", "path": "/api/v1/auth/login", "status": 200,
  "set_cookie": "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6…}; HttpOnly; Max-Age=86400; Path=/; SameSite=lax",
  "body": "{\"access_token\":\"eyJ…sub\":\"admin\"…\",\"token_type\":\"bearer\"}" }
```
`artifacts/latest-rss.json`: `{ "path": "/api/v1/rss", "status": 200, "body": "[]" }`

Variant 2 — pre-auth `/setup/complete` takeover
(`artifacts/setup-status.json` → `setup-complete.json` → `setup-login.json`):
```json
{ "path": "/api/v1/setup/status", "status": 200, "body": "{\"need_setup\":true,\"version\":\"3.2.8\"}" }
{ "path": "/api/v1/setup/complete", "status": 200, "body": "{\"status\":true,…\"Setup completed successfully.\"…}" }
{ "path": "/api/v1/auth/login", "status": 200, "set_cookie": "token=eyJ…sub\":\"pwned\"…}" }
{ "path": "/api/v1/rss", "status": 200, "body": "[]" }
{ "path": "/api/v1/auth/login", "status": 401, "body": "{\"msg_en\":\"User not found\"}" }  // admin/adminadmin after takeover
```

DEV rule-out (`artifacts/latest-noauth-rss.json`):
`{ "path": "/api/v1/rss", "status": 401, "body": "{\"detail\":\"Unauthorized\"}" }`

**Environment:** Official `ghcr.io/estrellaxd/auto_bangumi` images on the host
Docker daemon; AutoBangumi backend (Python 3.13, uvicorn/FastAPI, SQLite) inside
Alpine containers; webui port 7892; HTTP probes executed in-container via
`docker exec python3` (the Docker bridge is not host-routable in this sandbox).

## Recommendations / Next Steps

1. **Remove the hard-coded default admin seeding** in `add_default_user()`
   (require the setup wizard to create the first admin, or generate a random
   one-time bootstrap password surfaced once to the operator).
2. **Gate `POST /api/v1/auth/login` behind setup completion**
   (`config/.setup_complete` sentinel) so the default/seeded credentials cannot
   be used before initial configuration — closes Variant 1.
3. **Bind a one-time setup token / bootstrap secret to
   `POST /api/v1/setup/complete`** (or otherwise authenticate the first-run
   takeover) so an unauthenticated remote party cannot reset the admin account
   on a fresh instance — closes Variant 2. Critically, the user-account reset
   in `complete_setup` step 1 must not commit before the caller is authorized.
4. **Treat 3.2.8 and 3.3.0-beta.2 as still affected** by CVE-2026-58466 until a
   corrected fix that removes the default seeding and gates both `/auth/login`
   and `/setup/complete` is released.
5. **Regression tests:** assert a fresh empty DB never contains a login-capable
   account with a known password; assert `/auth/login` fails before setup
   completion; assert `/setup/complete` requires a setup token.

## Additional Notes

- **Bypass vs. alternate trigger:** Variant 1 is a **bypass** — the original
  default-credential exploit reproduces on the patched and latest version
  because the fix does not address it. Variant 2 is an **alternate trigger** —
  a different pre-auth entry point (`/api/v1/setup/complete`) reaching the same
  fresh-instance admin-takeover impact; it is arguably a distinct CWE-306
  first-run-takeover but is tightly coupled to the CVE root cause (it relies on
  the auto-seeded `admin` row existing and on the absence of a setup gate on
  `/auth/login`), and any fix to the default-credentials CVE must cover it.
- **Idempotency:** `reproduction_steps.sh` was executed twice consecutively;
  both runs exited `0` with identical positive evidence
  (`LATEST_LOGIN_OK=1`, `LATEST_ADMIN_OK=1`, `BETA_LOGIN_OK=1`,
  `SETUP_TAKEOVER_OK=1`, `DEV_BYPASS_ACTIVE=0`). All containers/volumes are
  removed on entry and exit (`trap cleanup EXIT`); no leftover containers.
- **Trust boundary:** Both variants cross the network trust boundary
  (unauthenticated remote HTTP → admin API) on a fresh, internet-exposed
  instance — the standard deployment exposed port 7892. This is not
  user-self-attack; it is remote unauthenticated takeover.
- **Scope honesty:** The DEV_AUTH_BYPASS path was tested and **ruled out** for
  the official images rather than claimed, to avoid over-stating the finding.
