# Root Cause Analysis — CVE-2026-58466

## Summary

AutoBangumi (a FastAPI-based bangumi/auto-download manager) seeds a hard-coded
default administrator account — `admin` / `adminadmin` — whenever its users
database table is empty. The seeding happens unconditionally on startup via
`add_default_user()` in `backend/src/module/database/user.py`. Because the
credentials are hard-coded and publicly visible in the source, an unauthenticated
remote attacker can submit them to the real authentication endpoint
(`POST /api/v1/auth/login`) on a freshly deployed instance and receive a valid
administrator JWT, gaining full administrative control of the application
(RSS feed configuration, downloader configuration, server logs, and every
authenticated API endpoint).

## Impact

- **Package/component affected:** AutoBangumi backend —
  `module/database/user.py` (`UserDatabase.add_default_user`), invoked from
  `module/update/startup.py` (`first_run`/`start_up`) during
  `module/core/program.py` startup.
- **Affected versions:** AutoBangumi **< 3.2.8** (reproduced on the official
  Docker image `ghcr.io/estrellaxd/auto_bangumi:3.2.6`).
- **Risk level:** Critical (CVSS v3 max 9.8 / v4 9.3). CWE-1392 Use of Default
  Credentials. Remote, unauthenticated, low complexity, full
  confidentiality/integrity/availability impact on the application.
- **Consequences:** Complete administrative takeover of a fresh AutoBangumi
  instance — an attacker can read/change RSS feeds, reconfigure the downloader
  (including credentials), read server logs, and exercise all authenticated
  API endpoints.

## Impact Parity

- **Disclosed/claimed maximum impact:** Unauthenticated attacker authenticates
  as administrator using publicly known default credentials and gains full
  administrative access (authz bypass / default credentials).
- **Reproduced impact from this run:** Full parity. Against the real
  AutoBangumi 3.2.6 product (official Docker image, fresh empty database) the
  default credentials `admin`/`adminadmin` were submitted to
  `POST /api/v1/auth/login` and returned **HTTP 200 + an admin JWT**
  (`sub: admin`). That JWT, sent as the `token` session cookie, granted access
  to admin-only endpoints (`/api/v1/rss` returned the RSS config list,
  `/api/v1/log` returned the server log stream) — proving full administrative
  access via the remote API.
- **Parity:** `full`
- **Not demonstrated:** N/A — the claimed impact (default-credential
  authentication bypass → admin access) was reproduced end-to-end through the
  real remote API surface.

## Root Cause

`UserDatabase.add_default_user()` queries the `users` table; if it is empty it
inserts a single user with hard-coded credentials:

```python
# backend/src/module/database/user.py
def add_default_user(self):
    statement = select(User)
    ...
    if len(users) != 0:
        return
    user = User(username="admin", password=get_password_hash("adminadmin"))
    self.session.add(user)
    self.session.commit()
    logger.info("[Database] Created default admin user")
```

This method is called on every startup from
`module/update/startup.py` (`start_up` and `first_run`), which are invoked by
`module/core/program.py` during the FastAPI lifespan startup. On a fresh
deployment (no persisted database) the table is empty, so the default
`admin`/`adminadmin` account is created. The login endpoint
(`module/api/auth.py`, `POST /api/v1/auth/login`) validates the submitted
credentials against the database via `UserDatabase.auth_user`, which uses
`verify_password`. Because the seeded password hash matches `adminadmin`, the
default credentials authenticate successfully and a JWT (`sub: admin`) is
issued. The session is registered in the in-memory `active_user` map, and the
`get_current_user` dependency accepts the `token` cookie for all protected
endpoints.

There is no forced password change, no setup-completion gate on the login
endpoint, and no randomization of the default password, so the publicly known
credentials remain valid until an administrator manually changes them through
the (unauthenticated-only-before-setup) setup wizard.

**Fix commit referenced by the advisory:**
`487bdfec545e805ae416e6ddf28651bd274d6a73` ("fix(api): harden pre-auth setup
endpoints (#1041, #1044)"). Note: inspection of that commit shows it hardens
the **SSRF** behavior of the pre-auth `/setup/test-*` endpoints and qBittorrent
5.2 login compatibility — it does **not** remove or randomize the default
`admin`/`adminadmin` account. Correspondingly, the negative control below shows
the default credentials remain exploitable in 3.2.8.

## Reproduction Steps

1. The self-contained script is
   **`bundle/repro/reproduction_steps.sh`**.
2. What it does:
   - Pulls the official AutoBangumi Docker images
     `ghcr.io/estrellaxd/auto_bangumi:3.2.6` (vulnerable) and
     `:3.2.8` (claimed patch).
   - For the vulnerable version, starts **two clean instances** each with a
     fresh empty Docker volume (so the users table is empty and
     `add_default_user()` triggers), waits for `Application startup complete`,
     and captures the startup log (which records
     `[Database] Created default admin user`).
   - Drives the **real remote API** from inside each container
     (the Docker bridge is not routable from the sandbox host, so HTTP probes
     are executed via `docker exec` against `127.0.0.1:7892`):
     `POST /api/v1/auth/login` with form `username=admin&password=adminadmin`.
   - On a 200 + JWT, reuses the `token` session cookie to call the admin-only
     endpoints `GET /api/v1/rss` and `GET /api/v1/log`.
   - Runs the same flow against `:3.2.8` as a negative control (two attempts).
   - Writes all HTTP request/response artifacts to `bundle/artifacts/http/`,
     startup logs to `bundle/logs/`, and the runtime manifest to
     `bundle/repro/runtime_manifest.json`.
3. Expected evidence of reproduction:
   - Startup log line `[Database] Created default admin user`.
   - Login response **HTTP 200** with `access_token` JWT and `set-cookie:
     token=<JWT>; HttpOnly`.
   - JWT payload decodes to `{"sub":"admin", ...}`.
   - `GET /api/v1/rss` → **200** `[]` (authenticated RSS config).
   - `GET /api/v1/log` → **200** server log text (authenticated).

## Evidence

- **Run log:** `bundle/logs/run.log` (full execution transcript, both runs).
- **Startup logs:** `bundle/logs/vuln-1-startup.log`,
  `bundle/logs/vuln-2-startup.log`, `bundle/logs/fixed-1-startup.log`,
  `bundle/logs/fixed-2-startup.log`.
- **HTTP artifacts:** `bundle/artifacts/http/vuln-{1,2}-login.json`,
  `vuln-{1,2}-rss.json`, `vuln-{1,2}-log.json`,
  `fixed-{1,2}-login.json`.
- **Runtime manifest:** `bundle/repro/runtime_manifest.json`.

Key excerpts (vulnerable 3.2.6, attempt 1):

Startup seeding:
```
[Database] Schema version is now 9.
[Database] Created default admin user
[Core] No db file exists, create database file.
Application startup complete.
Uvicorn running on http://0.0.0.0:7892
```

Login with default credentials (`bundle/artifacts/http/vuln-1-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\"}"
}
```
The JWT payload decodes to `{"sub":"admin","exp":...}` — authenticated as the
administrator.

Admin-only endpoint access with the session cookie
(`bundle/artifacts/http/vuln-1-rss.json`, `vuln-1-log.json`):
```json
{ "method": "GET", "path": "/api/v1/rss", "status": 200, "body": "[]" }
{ "method": "GET", "path": "/api/v1/log", "status": 200, "body": "[2026-07-03 ...] INFO ... Version 3.2.6 ..." }
```

**Environment:** Official Docker images on the host Docker daemon
(client 29.1.3); AutoBangumi 3.2.6 backend (Python 3.13, uvicorn/FastAPI,
SQLite) inside Alpine containers; webui port 7892; HTTP probes executed
in-container via `docker exec python3`.

**Negative control (3.2.8):** The 3.2.8 image **also** logs
`[Database] Created default admin user` on a fresh database **and** accepts the
`admin`/`adminadmin` login with **HTTP 200 + an admin JWT** in both attempts
(`bundle/artifacts/http/fixed-{1,2}-login.json`). This indicates the
advisory-referenced fix commit (`487bdfec`) addresses the related SSRF issue
(#1041) and does not remediate the hard-coded default-credentials seeding; the
`add_default_user()` source is byte-identical from 3.2.6 through HEAD.

## Recommendations / Next Steps

- **Primary fix:** Do not seed a hard-coded default administrator. Either (a)
  require the initial setup wizard to create the first admin account with a
  user-chosen password before any login is possible, or (b) generate a random
  one-time bootstrap password and display it once in the startup log / a
  bootstrap file, never reusing a public constant.
- **Defense in depth:** Gate `POST /api/v1/auth/login` behind setup completion
  (the existing `.setup_complete` sentinel) so login is rejected until the
  operator has finished initial configuration.
- **Upgrade guidance:** Operators should immediately change the admin password
  on any deployed instance and avoid exposing port 7892 to untrusted networks.
  Treat 3.2.8 as still affected for this specific CVE until a corrected fix
  that removes the default seeding is released.
- **Testing:** Add a regression test asserting that a fresh empty database
  never contains a login-capable account with a known password, and that
  `POST /api/v1/auth/login` fails before setup completion.

## Additional Notes

- **Idempotency:** The script removes its containers/volumes on entry and exit
  (`trap cleanup EXIT`) and was executed twice consecutively; both runs exited
  `0` with identical positive evidence (`VULN_LOGIN_OK=1`,
  `VULN_ADMIN_ACCESS_OK=1`).
- **Networking note:** In this sandbox the Docker bridge network is not
  routable from the host, so port-mapped requests from the host fail with
  "connection refused". The script therefore performs all HTTP probes from
  inside the container via `docker exec`, exercising the same real
  `127.0.0.1:7892` listener that uvicorn serves — this is the genuine
  application HTTP surface, not a mock.
- **Scope note:** The claim surface (`api_remote`) was satisfied through the
  real authentication endpoint and real admin API endpoints of the running
  product; no sanitizers were used (`sanitizer_used=false`); the JWT is a real
  HS256 token signed by the running application.
- **Limitation:** The default credentials were located in the source
  (`admin`/`adminadmin`) rather than supplied by the NVD entry, as the ticket
  noted the NVD entry omits them; they are confirmed correct by the runtime
  login success.
