Ctrl K
ComunidadGuías…

docs/SECURITY-AUDIT-2026-10-08.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Metis AI — Security-Audit (Code, Konfiguration, dynamisches Pentesting)

- **Datum:** 2026-10-08
- **Auditierte Revision:** `b97e22889505f342d647806bef5cecca8d097b5c` (package.json `version` 1.0.12)
- **Repository:** https://github.com/f1shyondrugs/metis.git (`/root/metis-ai`)
- **Methode:** statische Codeanalyse + Konfigurationsprüfung + dynamisches Pentesting gegen eine isolierte, frisch gebaute Testinstanz.
- **Testinstanz:** separater Baum aus `git archive b97e228`, eigene Ports (Web 3300 / MCP 8799), eigene leere SQLite-DB, Test-Secrets, Bind `127.0.0.1`. Keine Produktionsdaten verändert, keine Produktionsdienste gestoppt.
- **Scope-Grenzen eingehalten:** nur das autorisierte Metis-Projekt und die eigene Testinstanz; keine Angriffe gegen Provider, fremde Websites oder verbundene Nutzergeräte; nur künstliche Test-Accounts/Secrets; keine DoS-Tests gegen Produktion.

> Dieser Bericht dokumentiert ausschließlich. Es wurden **keine Produkt-Fixes** implementiert.

> **Nachprüfung des ursprünglichen Produktstands:** [SECURITY-AUDIT-2026-10-08-RECHECK.md](./SECURITY-AUDIT-2026-10-08-RECHECK.md) prüft `b431efb`. Ihre Status-/Reichweitenangaben sind maßgeblich: MA-03 bis zur Root-MCP-Shell mit künstlichen Markern bestätigt; pauschale Nicht-Root-, Docker-, Logout- und Host-RCE-Aussagen unten werden dort korrigiert. Dieser Erstbericht bleibt als historischer Beleg erhalten.

> **Umgesetzte Fixes:** [SECURITY-FIXES-2026-10-09.md](./SECURITY-FIXES-2026-10-09.md) beschreibt den behobenen Kandidaten, Regressionen, Betriebsänderungen und verbleibende Plattformgrenzen.

---

## 1. Architektur & Angriffsflächen

| Komponente | Rolle | Vertrauensgrenze |
| --- | --- | --- |
| Next.js App (`server.mjs`, `app/api/**`, ~125 Routen) | Web-UI + REST + WS | Session-Cookie `ai_chat_auth` (DB-Session) |
| Worker (`worker.ts`) | Agentenläufe, Tool-Ausführung | internes Bearer/Run-Lease |
| MCP-Gateway (`lib/mcp-core/*`, Port 8787) | Tool-Gateway | `MCP_BEARER_TOKEN` / signierte Metis-Session |
| Remote-Client (`remote-client/`, WS `/ws/remote-client`) | Geräte-Fernsteuerung | Enrollment-Token + Client-Credential |
| SQLite (`data/chat.sqlite`) | Chats, User, Sessions, Secrets | Dateisystem/OS |
| Installer (`install.sh`, `install/*`, `docker-compose.yml`) | Bootstrap | Host-Operator |

**Vertrauensgrenzen:** (a) anonym ↔ authentifiziert (Session-Cookie), (b) Nutzer ↔ Nutzer (`owner_id` in DB, OS-User bei Agent-Ausführung), (c) App ↔ MCP-Gateway (Bearer/Session), (d) Server ↔ Remote-Client (Enrollment/Credential), (e) Agent-Tools ↔ Host (OS-User + `allowRootAgents`).

**Bedrohungsmodell (bewertet):** nicht-authentifizierter Angreifer · gering privilegierter authentifizierter Nutzer · Nutzer-gegen-Nutzer-Zugriff · bösartige Uploads/Webseiten/Tool-Ergebnisse/MCP-Inhalte · kompromittierter Remote-Client/MCP-Server. Terminal- und Tool-Ausführung sind gewollte Features; bewertet wurde die Umgehung von Rechte-, Freigabe- und Nutzergrenzen.

---

## 2. Übersicht der Findings

| ID | Titel | Schwere | Typ | Frische Installation betroffen | Status |
| --- | --- | --- | --- | --- | --- |
| MA-01 | Unauthentifizierte First-Run-Admin-Erstellung (Account-Takeover → Host-RCE) | **Hoch** | Schwachstelle | Ja, bei Bind ≠ localhost vor Onboarding | Bestätigt (dynamisch) |
| MA-02 | Rate-Limit-Umgehung über client-kontrolliertes `X-Real-IP`/letzter XFF-Hop | **Mittel** | Schwachstelle | Ja, bei Installation ohne vertrauenswürdigen Proxy | Bestätigt (dynamisch) |
| MA-03 | Fehlende Agent-Ausführungsisolation zwischen App-Nutzern bei Root-Installation | **Hoch** | Schwachstelle (Default-abhängig) | Ja, Standard-Root-Installation mit Mehrbenutzer | Plausibel + teilverifiziert |
| MA-04 | Unauthentifizierter LLM-Kosten-/Ressourcen-Endpunkt `/api/process-a` | **Mittel** | Schwachstelle (Dead Code) | Nur wenn `OPENAI_API_KEY` gesetzt | Bestätigt reachable |
| MA-05 | Pre-Auth-Offenlegung von Host-OS-Benutzernamen + Home-Pfaden via `/api/setup` | **Niedrig** | Info-Disclosure | Ja, während Onboarding-Fenster | Bestätigt (dynamisch) |
| MA-H1 | WS `/api/browser/stream` ohne Origin-Allowlist (nur SameSite=Lax) | Hardening | Empfehlung | — | Beobachtet |
| MA-H2 | Kein konfigurierbarer Trusted-Proxy-Hop / Header-Trust abschaltbar | Hardening | Empfehlung | — | Beobachtet |
| MA-H3 | Session-Logout invalidiert nur die aktuelle Session, keine Rotation | Hardening | Empfehlung | — | Beobachtet |
| MA-H4 | Lauf als root + `allowRootAgents`-Default für `/root`-Installationen | Hardening | Empfehlung | — | Beobachtet |

---

## 3. Findings (Belegstandard)

### MA-01 — Unauthentifizierte First-Run-Admin-Erstellung

- **Schwere:** Hoch. Begründung: vollständige Account-Übernahme eines frischen Deployments; da Admin-Agenten Terminal-/Dateitools besitzen, folgt effektiv Host-RCE.
- **Betroffen:** `app/api/setup/route.ts` (`POST`, `action === "bootstrap"`), `lib/setup.ts` (`getSetupStatus`/`isSetupComplete`), `lib/admin-users.ts` (`createManagedUser`).
- **Angreiferrolle/Voraussetzung:** nicht-authentifiziert; Netzwerkerreichbarkeit der App **bevor** der Betreiber das Onboarding abgeschlossen hat. Default-Bind ist `127.0.0.1`; die `.env.example`/README nennen `AI_CHAT_HOST=0.0.0.0` aber als unterstützte LAN-Option.
- **Ursache/Datenfluss:** Der Bootstrap-Zweig ist bewusst vor dem Auth-Check platziert und nur durch `getSetupStatus().hasUsers === false` geschützt (= „es existiert noch kein Nutzer"). Es gibt **kein Out-of-Band-Geheimnis** (kein Setup-Token in Server-Log/Konsole, keine Localhost-Bindung des Bootstraps). Der erste Request gewinnt und erhält `isAdmin: true` plus gültige Session.

```47:68:app/api/setup/route.ts
  if (action === "bootstrap") {
    const status = getSetupStatus();
    if (status.hasUsers) {
      return Response.json({ error: "Setup already has an account." }, { status: 409 });
    }
    try {
      const user = createManagedUser({
        username: body.username || "",
        password: body.password || "",
        isAdmin: true,
        osUsername: body.osUsername?.trim() || undefined,
      });
      markSetupIncomplete();
      const session = authenticateUser(user.username, body.password || "");
```

- **Reproduktion (dynamisch, Testinstanz, leere DB):**
  ```bash
  curl -i -X POST http://127.0.0.1:3300/api/setup \
    -H 'Content-Type: application/json' \
    -d '{"action":"bootstrap","username":"attacker","password":"attackerpass123"}'
  ```
- **Erwartet:** First-Run nur lokal/konsolengebunden oder per Einmal-Token.
- **Beobachtet:** `HTTP/1.1 201 Created`, `Set-Cookie: ai_chat_auth=…; HttpOnly; SameSite=lax`, Body `{"ok":true,"user":{…,"isAdmin":true,"workspaceRoot":"…/workspace"}}`. Folge-Request mit dem Cookie an `/api/chats` ⇒ `200`.
- **Auswirkung/Reichweite:** Übernahme des ersten (Admin-)Accounts; Aussperren des Betreibers; über Agent-Tools Zugriff auf Host und Secrets.
- **Gültigkeit frische Installation:** ja, sobald die App vor Onboarding netzwerkerreichbar ist (dokumentierte LAN-Option, Reverse-Proxy-Setups, Container mit offenem Port).
- **Fix-Vorschläge:**
  1. Bootstrap nur zulassen, wenn der Request von Loopback kommt **oder** ein beim Erststart generiertes, ins Server-Log/`data/`-Datei geschriebenes Einmal-Setup-Token mitgegeben wird (vgl. Jupyter/Grafana-Muster).
  2. Alternativ Bootstrap an `AI_CHAT_BIND`/`AI_CHAT_HOST === 127.0.0.1` koppeln bzw. bei Nicht-Loopback einen Token erzwingen.
  3. Rate-Limit + generische Fehler auch auf `action === "bootstrap"`.
- **Regressionstest:** `tests/setup-onboarding.test.ts` erweitern: (a) Bootstrap über nicht-Loopback-Request ohne Token ⇒ 401/403; (b) mit korrektem Einmal-Token ⇒ 201; (c) zweiter Bootstrap ⇒ 409.
- **Verifikationsstatus:** Bestätigt.

---

### MA-02 — Rate-Limit-Umgehung über client-kontrollierte IP-Header

- **Schwere:** Mittel. Begründung: Login- und Share-Passwort-Rate-Limits sind als Brute-Force-Schutz dokumentiert (SECURITY.md Pkt. 8), lassen sich aber ohne vertrauenswürdigen Proxy trivial umgehen.
- **Betroffen:** `lib/rate-limit.ts` (`requestClientAddress`), `app/api/auth/route.ts`, `app/api/share/route.ts`.
- **Angreiferrolle/Voraussetzung:** nicht-authentifiziert; App ist direkt erreichbar (kein Proxy, der `x-real-ip`/XFF überschreibt).
- **Ursache/Datenfluss:** Der Limiter schlüsselt auf `x-real-ip` bzw. den **letzten** `X-Forwarded-For`-Hop. Beide Header sind bei einer Direktverbindung vollständig vom Client kontrolliert. Da auch der User-Schlüssel `auth:user:${address}:${username}` die Adresse enthält, rotiert der Angreifer einfach die IP und setzt beide Fenster zurück.

```34:43:lib/rate-limit.ts
export function requestClientAddress(req: Request) {
  const realIp = req.headers.get("x-real-ip")?.trim();
  if (realIp) return realIp.slice(0, 128);
  const forwarded = req.headers.get("x-forwarded-for");
  if (forwarded) {
    const hops = forwarded.split(",").map((part) => part.trim()).filter(Boolean);
    return (hops.at(-1) || "unknown").slice(0, 128);
  }
  return "unknown";
}
```

- **Reproduktion (dynamisch):** 35 Fehllogins mit konstantem `X-Real-IP` vs. 35 mit rotierendem `X-Real-IP`.
  ```bash
  for i in $(seq 1 35); do curl -s -o /dev/null -w '%{http_code}\n' -X POST \
    http://127.0.0.1:3300/api/auth -H 'Content-Type: application/json' \
    -H "X-Real-IP: 10.0.0.$i" -d '{"username":"admin","password":"wrong"}'; done | sort | uniq -c
  ```
- **Erwartet:** nach wenigen Versuchen `429`.
- **Beobachtet:** konstante IP ⇒ `401`×10 dann `429`×25 (Limit greift). Rotierende IP ⇒ `401`×35, **kein** `429`. Brute-Force damit ungedrosselt.
- **Auswirkung/Reichweite:** unbegrenzter Passwort-Brute-Force auf das Login (und Share-Passwörter). Schwache Passwörter werden angreifbar; die dokumentierte Schutzmaßnahme trägt nicht.
- **Gültigkeit frische Installation:** ja, für jede Direktexposition ohne Proxy, der die Header normalisiert.
- **Fix-Vorschläge:**
  1. Header-Trust nur aktivieren, wenn explizit konfiguriert (`TRUSTED_PROXY_HOPS`/`TRUST_PROXY=true`); sonst `request.socket.remoteAddress` verwenden.
  2. Bei konfiguriertem Proxy den Hop anhand bekannter Proxy-CIDRs auswählen statt blind letzten/`x-real-ip`.
  3. Zusätzlich global (nicht nur pro Adresse) ein konservatives Login-Fehler-Budget.
- **Regressionstest:** neuer Test in `tests/auth.test.ts`: ohne `TRUST_PROXY` wird `x-real-ip` ignoriert und `remoteAddress` als Key genutzt; rotierende `X-Real-IP` triggert weiterhin `429`.
- **Verifikationsstatus:** Bestätigt.

---

### MA-03 — Fehlende Agent-Ausführungsisolation zwischen App-Nutzern (Root-Installation)

- **Schwere:** Hoch für Mehrbenutzer-Installationen; nicht relevant für Einzelbenutzer. Default-abhängig.
- **Betroffen:** `lib/config.ts` (`defaultAllowRootAgents`), `lib/admin-users.ts` (`createManagedUser`), `lib/user-access.ts` (`getUserAccess`/`getUserExecutionIdentity`), `lib/uploads.ts` (`uploadsRoot` → `getAgentCwd`).
- **Angreiferrolle/Voraussetzung:** gering privilegierter, authentifizierter Nutzer (kein Admin) auf einer Instanz, die als `root` in `/root` installiert ist (so richten der Linux-Installer und die systemd-Units dieser Umgebung die App ein) und mehrere App-Nutzer hat, die vom Admin **ohne** eigenen `osUsername`/`workspaceRoot` angelegt wurden.
- **Ursache/Datenfluss:**
  - `createManagedUser` setzt für Nicht-Erst-Nutzer `osUsername = undefined` und `workspaceRoot = config.agentCwd` (geteiltes Verzeichnis).
  - `defaultAllowRootAgents()` aktiviert Root-Agenten automatisch, wenn als uid 0 in `/root` gelaufen wird.
  - `getUserExecutionIdentity` liefert dann für **jeden** Nutzer ohne OS-Mapping die Root-Identität mit demselben `workspaceRoot`.

```32:39:lib/config.ts
function defaultAllowRootAgents() {
  const flagged = env("AI_CHAT_ALLOW_ROOT_AGENTS");
  if (flagged) return booleanEnv("AI_CHAT_ALLOW_ROOT_AGENTS");
  const uid = typeof process.getuid === "function" ? process.getuid() : -1;
  const resolved = path.resolve(agentCwd);
  const rootHome = path.resolve("/root");
  return uid === 0 && (resolved === rootHome || resolved.startsWith(`${rootHome}${path.sep}`));
}
```

```165:176:lib/user-access.ts
export function getUserExecutionIdentity(userId?: string): UserExecutionIdentity | undefined {
  const access = getUserAccess(userId);
  if (!access.osUsername && config.allowRootAgents && isRootWorkspace(access.workspaceRoot)) {
    return { username: "root", uid: 0, gid: 0, home: "/root", workspaceRoot: access.workspaceRoot };
  }
  if (!access.osUsername) return undefined;
```

- **Datenbank-Isolation hält** (positiv): Chats/Notes/Memories/Uploads sind in der App-Schicht über `owner_id` getrennt (siehe MA-Negativtests). Die Lücke betrifft die **Ausführungsschicht**: über Agent-Terminal/Datei-Tools läuft Nutzer B als root im selben Workspace und liest Uploads/`chat.sqlite` (inkl. aller `owner_id`-Zeilen, Session-Token-Hashes), `.env` (Secrets) und beliebige Hostdateien — die DB-`owner_id`-Prüfung wird dadurch irrelevant.
- **Positiver Gegenbefund (fail-safe):** Ohne `allowRootAgents` und ohne OS-Mapping wirft `requireUserExecutionIdentity` („no valid OS user mapping") — dann sind Agentenläufe für solche Nutzer blockiert (sicher). Die Lücke entsteht erst durch den Root-Default.
- **Reproduktion/Beleg:** In der Testinstanz erhielten Admin `attacker` und Nicht-Admin `victimB` denselben `workspaceRoot` `/root/metis-audit/workspace` (bestätigt via `/api/setup` und `POST /api/admin/users`). Der Endnachweis eines Cross-User-Reads via Agent erfordert Worker+Provider-Key und wurde daher nicht live ausgeführt; die Code-Kette ist eindeutig.
- **Gültigkeit frische Installation:** ja, für die Standard-Root-Installation mit mehreren App-Nutzern.
- **Fix-Vorschläge:**
  1. Mehrbenutzer ohne eindeutige OS-User-Bindung verbieten (hart fehlschlagen statt auf Root zu kollabieren), sobald mehr als ein App-Nutzer existiert.
  2. `createManagedUser` sollte pro Nutzer einen distinkten `workspaceRoot` und eine verpflichtende OS-User-Bindung erzwingen (oder Agentenläufe deaktivieren).
  3. `allowRootAgents` nie implizit aus „läuft als root in /root" ableiten; nur explizit via Env, mit deutlicher Warnung.
- **Regressionstest:** `tests/user-isolation.test.ts`/`tests/owner-boundary.test.ts` erweitern: zweiter Nutzer ohne OS-Mapping bei `allowRootAgents=true` ⇒ `getUserExecutionIdentity` darf **nicht** Root mit geteiltem Workspace liefern.
- **Verifikationsstatus:** Plausibler, codebelegter Verdacht (Workspace-Sharing dynamisch bestätigt; Root-Exec-Read nicht live ausgeführt).

---

### MA-04 — Unauthentifizierter LLM-Kosten-/Ressourcen-Endpunkt `/api/process-a`

- **Schwere:** Mittel, wenn `OPENAI_API_KEY`/`OPENAI_MODEL` gesetzt sind; sonst latent.
- **Betroffen:** `app/api/process-a/route.ts`.
- **Angreiferrolle/Voraussetzung:** nicht-authentifiziert; App erreichbar; serverseitiger Provider-Key konfiguriert.
- **Ursache/Datenfluss:** Die Route hat **keinerlei** Auth-Prüfung und ruft `generateObject` (200 Objekte, temperature 0.8) mit dem Server-Key auf. Nutzer-`topic` fließt direkt in den Prompt.

```16:32:app/api/process-a/route.ts
export async function POST(request: Request) {
  try {
    const input = requestSchema.parse(await request.json());
    const apiKey = process.env.OPENAI_API_KEY || process.env.AI_GATEWAY_API_KEY;
    const modelId = process.env.OPENAI_MODEL || process.env.AI_MODEL;
    if (!apiKey || !modelId) {
      return Response.json({ error: "Process A is missing OPENAI_API_KEY and OPENAI_MODEL in the environment." }, { status: 503 });
    }
    const provider = createOpenAI({ apiKey, baseURL: process.env.OPENAI_BASE_URL });
    const { object } = await generateObject({ model: provider(modelId), schema: resultSchema, temperature: 0.8, prompt: `…"${input.topic}"…` });
```

- **Reproduktion:** `curl -X POST http://127.0.0.1:3300/api/process-a -H 'Content-Type: application/json' -d '{"topic":"abc","languages":["en"]}'` ⇒ in der Testinstanz `503` (kein Key), d. h. **erreichbar ohne Auth**; mit Key würde jede Anfrage eine teure LLM-Generierung auslösen.
- **Auswirkung:** unauthentifizierter Kosten-/Quota-Verbrauch (Rechnung des Betreibers), Ressourcen-DoS; Prompt-Injection über `topic` (geringe Wirkung, eigener Key).
- **Gültigkeit frische Installation:** nur bei gesetztem Provider-Key in der Server-Umgebung. Wirkt wie eingeschleuster, produktfremder „Influencer-Outreach"-Code.
- **Fix-Vorschläge:** Route entfernen (gehört nicht zum Produkt) oder hinter `isAuthenticated` + Rate-Limit stellen.
- **Regressionstest:** Test, der sicherstellt, dass `/api/process-a` ohne Session `401` liefert (bzw. die Route nicht existiert).
- **Verifikationsstatus:** Bestätigt (unauth erreichbar).

---

### MA-05 — Pre-Auth-Offenlegung von Host-OS-Benutzern via `/api/setup`

- **Schwere:** Niedrig.
- **Betroffen:** `app/api/setup/route.ts` (`GET`), `lib/user-access.ts` (`listHostOsUsers`).
- **Ursache/Datenfluss:** Vor dem ersten Nutzer (`needed && !hasUsers`) liefert `GET /api/setup` unauthentifiziert die Liste der Host-OS-Benutzer inkl. Home-Pfade.

```24:35:app/api/setup/route.ts
export async function GET(req: Request) {
  const ownerId = (await getAuthenticatedUserId(req)) ?? undefined;
  const status = getSetupStatus(ownerId);
  const canListOsUsers = status.needed && (!status.hasUsers || (ownerId && isHostAdmin(ownerId)));
  return Response.json({ ...status, platform: hostPlatform(),
    osUsers: canListOsUsers ? listHostOsUsers().map(({ username, home }) => ({ username, home })) : [], … });
}
```

- **Beobachtet:** Die ursprüngliche Testinstanz gab unauthentifiziert lokale Benutzernamen samt Home-Pfaden zurück; konkrete Hostkonten sind hier anonymisiert. Die Nachprüfung verwendet ausschließlich eine künstliche OS-Benutzerliste.
- **Auswirkung:** Enumeration lokaler Konten/Pfade im Onboarding-Fenster; nützlich für Folgeangriffe (zusammen mit MA-01).
- **Fix-Vorschläge:** OS-User-Liste erst nach Loopback-Bootstrap/Token bzw. nur für authentifizierte Host-Admins zurückgeben.
- **Regressionstest:** Test, der prüft, dass `osUsers` bei nicht-Loopback/unauth leer ist.
- **Verifikationsstatus:** Bestätigt.

---

## 4. Hardening-Empfehlungen (keine bestätigten Schwachstellen)

- **MA-H1 — WS-Origin:** `server.mjs` `upgrade` für `/api/browser/stream` prüft Session-Cookie, aber **keinen `Origin`**. SameSite=Lax verhindert hier das Mitsenden des Cookies bei Cross-Site-WS-Handshakes (daher kein bestätigter CSWSH), dennoch empfohlen: explizite Origin-Allowlist analog `corsAllowedOrigins`.
- **MA-H2 — Trusted Proxy:** konfigurierbares Proxy-Trust-Modell statt blindem Header-Vertrauen (siehe MA-02).
- **MA-H3 — Session-Lifecycle:** `DELETE /api/auth` löscht nur das Cookie; serverseitig bleibt der Session-Datensatz 30 Tage gültig, keine Rotation nach Login. Empfehlung: Session-Row bei Logout invalidieren, optional Rotation.
- **MA-H4 — Root-Betrieb:** Betrieb als dedizierter Nicht-Root-Service-User empfehlen; `allowRootAgents` nur explizit (siehe MA-03).

---

## 5. Geprüfte Bereiche mit negativem Befund (robust)

| Bereich | Befund | Beleg |
| --- | --- | --- |
| IDOR/BOLA Chats | `owner_id`-Scoping durchgesetzt; Cross-User-Read ⇒ 404 (dynamisch), anon ⇒ 401 | `lib/db-store.ts` `getChat`/`getChatPage`; `app/api/chats/[id]/route.ts` |
| Share-Passwort | pbkdf2 + `timingSafeEqual`; `passwordHash` nie in `publicShare`/`publicChat` | `lib/db-store.ts` `verifySharePassword`/`publicShare` |
| SSRF (link-preview, voice) | Private-IP-Ranges, Metadata-Hosts, DNS-Rebinding (Re-Resolve pro Hop), Cross-Origin-Redirects blockiert; Credentials-in-URL verboten | `lib/url-security.ts` |
| Path Traversal (Uploads/Share-Attachment) | `basename`-Prüfung, `..`/`/`-Ablehnung, Prefix-Check auf Zielverzeichnis | `lib/uploads.ts` `resolveUploadPath`; `app/api/share/attachment/route.ts` |
| XSS (Markdown) | react-markdown ohne `rehype-raw` (kein Roh-HTML); `highlight.js` escaped Code | `components/markdown.tsx` |
| XSS (Mermaid) | `securityLevel: "strict"`, `htmlLabels: false`, mermaid v11 (interne DOMPurify) | `components/mermaid-diagram.tsx` |
| Secrets at rest | AES-256-GCM, Schlüssel verpflichtend (`AI_CHAT_SECRETS_KEY`), kein Default-Key | `lib/secrets.ts` |
| MCP-Gateway-Auth | Localhost ≠ Auth; Bearer/Signed-Session erforderlich; timing-safe; CORS-Allowlist | `lib/mcp-core/http-auth.mjs` |
| Remote-Enrollment | Permission-Mode an Token gebunden (`a_`/`u_`-Präfix + DB-Abgleich); Single-Use; TTL | `lib/remote-clients.ts` `consumeEnrollmentToken` |
| Remote-Authz | Modus/Capability/Allowlist/riskante-Befehle serverseitig vor WS-Aufruf | `lib/remote-clients.ts` `authorizeRemoteAction` |
| Installer-Defaults | `openssl rand` für `MCP_BEARER_TOKEN`/`AI_CHAT_SECRETS_KEY`; `.env` chmod 600; Release-SHA256-Verifikation | `install.sh`, `install/linux.sh` |
| Passwort-Hashing/Session | pbkdf2 120k, Token zufällig (32 B), nur Hash gespeichert, HttpOnly/SameSite=Lax, secure bei https | `lib/auth.ts`, `app/api/auth/route.ts` |

---

## 6. Testmatrix (frische Installation / Rollen / Varianten)

| Szenario | anon | Nutzer (gering) | Nutzer→fremde Daten | Admin | Ergebnis |
| --- | --- | --- | --- | --- | --- |
| Login (Direktexposition, ohne Proxy) | MA-02 Bypass | — | — | — | Schwachstelle |
| First-Run vor Onboarding (Bind ≠ localhost) | MA-01 Takeover | — | — | — | Schwachstelle |
| First-Run (Bind 127.0.0.1, Default) | nicht erreichbar | — | — | — | OK |
| Chat-Zugriff fremd | 401 | 404 | 404 | 404 (nur eigene) | Isolation OK |
| Agent-Ausführung (Root-Install, Mehrbenutzer) | — | läuft als root/shared WS | Cross-User möglich | — | MA-03 |
| Agent-Ausführung (Nicht-Root, kein OS-Mapping) | — | blockiert (fail-safe) | — | — | OK |
| `/api/process-a` | erreichbar (503/LLM) | — | — | — | MA-04 |
| `/api/setup` GET (Onboarding-Fenster) | OS-User-Leak | — | — | — | MA-05 |
| MCP-Gateway (localhost, kein Token) | abgelehnt | — | — | — | OK |

**Installationsvarianten bewertet:** Standard-Linux (systemd, root, `/root`) · Docker (`docker-compose.yml`, Nicht-Root-Workspace) · Bind 127.0.0.1 (Default) vs. 0.0.0.0 (LAN). **Nicht live getestet:** Windows-Per-Machine/UAC-Remote-Client (keine Windows-Umgebung), realer Agent-Lauf mit Provider-Key (MA-03-Endnachweis), macOS-Installer.

**Produktgarantien vs. Betreiberpflichten:** Das Produkt liefert selbst: Auth/Session, DB-`owner_id`-Isolation, Secret-Verschlüsselung, SSRF-/Path-/XSS-Abwehr, MCP-Bearer-Zwang, Installer-Secret-Generierung + Datei-Rechte. In Betreiberverantwortung bleiben: vertrauenswürdiger Proxy/WAF (MA-02), Netzwerkexposition erst nach Onboarding bzw. Loopback-First-Run (MA-01), OS-User-Isolation/Nicht-Root-Betrieb bei Mehrbenutzer (MA-03).

---

## 7. Priorisierte Umsetzungsreihenfolge

1. **MA-01** — Bootstrap auf Loopback/Einmal-Token beschränken (Account-Takeover, einfachster Full-Compromise).
2. **MA-03** — Mehrbenutzer ohne eindeutige OS-User-Isolation hart verbieten; `allowRootAgents` nicht implizit.
3. **MA-02** — konfigurierbares Trusted-Proxy-Modell; ohne Proxy `remoteAddress` nutzen.
4. **MA-04** — `/api/process-a` entfernen oder authentifizieren + rate-limiten.
5. **MA-05** — OS-User-Liste erst nach sicherem Bootstrap/für Admins.
6. **MA-H1–H4** — Origin-Allowlist für WS, Session-Invalidierung bei Logout, Nicht-Root-Betrieb dokumentieren/erzwingen.

---

## 8. Hinweise zu Dependencies

Keine erfundenen CVEs. Dependency-Meldungen wurden nicht als Findings gewertet, ohne erreichbaren Pfad nachzuweisen; ein erfolgreicher Scanner belegt keine Sicherheit. Für eine belastbare SCA sollte `pnpm audit --prod` gegen `pnpm-lock.yaml` mit anschließender Erreichbarkeitsprüfung je Treffer laufen (nicht Teil dieses dynamischen Audits).