# Code-level analysis — CVE-2026-56782 ## 1. Route registration `master/rest.go` wires the two endpoints onto the master HTTP container (the same server that serves the dashboard on port `8088`): ```go container.Handle("/api/dump", http.HandlerFunc(m.dump)) container.Handle("/api/restore", http.HandlerFunc(m.restore)) ``` These are plain `http.HandlerFunc` handlers — they are **not** behind the dashboard's cookie/session middleware. Each handler is responsible for its own authorization, which it delegates to `checkAdmin()`. ## 2. The fail-open check ```go func (m *Master) checkAdmin(request *http.Request) bool { if m.Config.Master.AdminAPIKey == "" { return true // (A) } if request.Header.Get("X-API-Key") == m.Config.Master.AdminAPIKey { return true // (B) } return false // (C) } ``` The intended model is (B)/(C): a request is admin iff it presents the configured key. But branch **(A)** short-circuits when the key is empty and returns `true` *before the header is ever read*. So when `admin_api_key` is unset: * a request with **no** `X-API-Key` → admin, * a request with a **wrong** `X-API-Key` → admin (the header is never compared). `config/config.toml` ships: ```toml # Secret key for admin APIs (SSL required). admin_api_key = "" ``` so a default deployment is in state (A): unauthenticated admin for anyone who can reach the port. ## 3. What each handler does once admin is granted ```go func (m *Master) dump(response http.ResponseWriter, request *http.Request) { if !m.checkAdmin(request) { writeError(response, 401, "unauthorized"); return } if request.Method != http.MethodGet { writeError(response, 405, ...); return } response.Header().Set("Content-Type", "application/octet-stream") // streams users, then items, then feedback (see §4) } func (m *Master) restore(response http.ResponseWriter, request *http.Request) { if !m.checkAdmin(request) { writeError(response, 401, "unauthorized"); return } if request.Method != http.MethodPost { writeError(response, 405, ...); return } stats, err := m.Restore(request.Body, nil) // overwrites the dataset from the body ... } ``` `dump` walks `GetUserStream` / `GetItemStream` / `GetFeedbackStream` straight out of the data store. `restore` feeds the request body back into the store. No further authorization, scoping, or confirmation is applied. ## 4. Dump stream format The body is a flat binary stream (`writeDump` / `readDump`): ``` section marker int64 LE : -1 = users -2 = items -3 = feedback 0 = EOF record int64 LE length prefix (> 0) followed by that many protobuf bytes ``` A reader distinguishes the two by sign: a non-positive int64 is a section marker / EOF, a positive int64 is the length of the next protobuf record. So the layout is: ``` [-1][len][User pb][len][User pb]... [-2][len][Item pb]... [-3][len][Feedback pb]... [0] ``` `exploit.py` parses exactly this to count records per section and to pull field #1 (the `UserId` / `ItemId` string, protobuf tag `0x0A`) from a few records as a sample — turning the raw 262 MB stream into a concrete "2079 users / 22320 items / 331901 feedback exfiltrated" impact statement. ## 5. The fix (0.5.10) `0.5.10` removes the `checkAdmin()` fail-open: an empty `admin_api_key` no longer returns `true`, so `/api/dump` and `/api/restore` reject unauthenticated requests (HTTP 401) regardless of configuration. This is the binary discriminator between **VULNERABLE (< 0.5.10)** and **PATCHED (≥ 0.5.10)** — the PoC's `restore_probe()` reports `401` against a patched server and `200/500` against a vulnerable one.