Files

13 KiB
Raw Permalink Blame History

Security- und Plausibilitäts-Review: Kundenportal & Authentifizierung

Hier ist das detaillierte Sicherheits-, Architektur- und Plausibilitäts-Review für das Kundenportal von webkulisse. Das Review analysiert den Ist-Zustand (Authentifizierung auf Basis von Better-Auth und TanStack Start / React Router) und liefert konkrete Handlungsempfehlungen, damit künftige Module (Rechnungsverwaltung und Produktabnahmen) modular, mandantensicher und ohne Spaghetti-Code integriert werden können.


1. Priorisierte Findings & Sicherheitsrisiken


🔴 KRITISCH: Fehlendes Mandanten- & Autorisierungskonzept für Multi-Kunden-Daten (IDOR-Gefahr)

  • Dateiverweis: src/db/schema.ts, src/lib/auth.ts, src/lib/session.ts

  • Problem & Risiko: Das aktuelle Schema speichert lediglich flache user-Einträge mit einer einfachen Rolle (role: 'user' | 'admin'). Es gibt keine Mandanten- bzw. Kundenentität (Organization, Customer oder Tenant).

    Wenn Rechnungen und Abnahmen direkt auf userId referenzieren:

    1. Kann ein Kunde nicht mehrere Mitarbeiter/Ansprechpartner haben (typischer B2B-Anwendungsfall: Inhaber + Buchhaltung).
    2. Besteht ohne standardisierte Mandanten-Middleware bei kommenden Server-Funktionen ein extremes Risiko für IDOR (Insecure Direct Object Reference) z. B. wenn ein Kunde über manipulierte IDs Rechnungen oder Abnahmen anderer Kunden abrufen oder freigeben kann.
  • Lösungsempfehlung: Einführung einer expliziten customer/organization-Tabelle und einer typsicheren Server-Function-Middleware, die bei jedem Datenzugriff den aktuellen Mandantenkontext prüft.

// src/db/schema.ts - Empfohlenes Schema-Fundament
export const customer = pgTable('customer', {
  id: text('id').primaryKey(),
  name: text('name').notNull(), // Firmenname z. B. 'Praxis Dr. Schmidt'
  customerNumber: text('customer_number').notNull().unique(), // z. B. KD-2026-001
  status: text('status').notNull().default('active'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

export const user = pgTable('user', {
  // ... bestehende Better-Auth Felder
  customerId: text('customer_id').references(() => customer.id, { onDelete: 'cascade' }),
})

🔴 KRITISCH: Devtools und sensible Debugger-Panels im Produktions-Bundle

  • Dateiverweis: src/routes/__root.tsx (Zeilen 94104), vite.config.ts (Zeile 14)
  • Problem & Risiko: <TanStackDevtools /> und <TanStackRouterDevtoolsPanel /> werden im RootDocument bedingungslos gerendert. Dadurch werden Router-Zustände, Context-Daten und interne Routing-Strukturen im Browser für jeden Besucher sichtbar.
  • Lösungsempfehlung: Devtools nur in der Entwicklungsumgebung laden und im Build strippen:
// src/routes/__root.tsx
{process.env.NODE_ENV !== 'production' ? (
  <TanStackDevtools
    config={{ position: 'bottom-right' }}
    plugins={[{ name: 'Tanstack Router', render: <TanStackRouterDevtoolsPanel /> }]}
  />
) : null}

🟠 HOCH: Kein Self-Service-Passwort-Reset / Unsichere initiale Passwort-Weitergabe

  • Dateiverweis: src/routes/_admin/admin.users.new.tsx (Zeilen 86101), src/routes/login.tsx
  • Problem & Risiko:
    1. Beim Erstellen eines Kunden (admin.users.new.tsx) muss der Admin ein Klartext-Passwort vergeben, welches als normales Input-Feld (type="text") sichtbar ist.
    2. Admins müssen dieses Initialpasswort unverschlüsselt (per Mail/Chat) an Kunden weitergeben.
    3. Es gibt im Kunden-Login keinen "Passwort vergessen?"-Flow (/reset-password).
    4. Kunden haben keine Möglichkeit, ihr Passwort nach dem Erst-Login selbstständig zu ändern.
  • Lösungsempfehlung:
    1. Einladungs-Link / Magic-Link Flow: Statt manueller Passwörter generiert der Admin einen zeitlich befristeten Aktivierungslink via Better-Auth Invitation/Reset-Token.
    2. Implementierung eines standardisierten Passwort vergessen-Flows im login.tsx.

🟠 HOCH: In-Memory Rate Limiting bei verteilten Deployments

  • Dateiverweis: src/lib/auth.ts (Zeilen 3950)
  • Problem & Risiko: Das Rate Limiting ist auf storage: 'memory' konfiguriert:
    rateLimit: {
      enabled: true,
      storage: 'memory',
      customRules: {
        '/sign-in/email': { window: 60, max: 5 },
        // ...
      }
    }
    
    In Serverless-, Container- oder Node-Cluster-Umgebungen (z. B. Nitro / Cloudflare / Node Multi-Worker) teilt sich jeder Worker einen eigenen Speicher. Bei Server-Neustarts oder mehreren Instanzen ist der Brute-Force-Schutz wirkungslos oder leicht zu umgehen.
  • Lösungsempfehlung: Anbindung eines persistenten Speichers (z. B. Redis / Upstash / Postgres-basiertes Rate Limiting) oder Absicherung über Cloudflare WAF / Reverse Proxy.

🟡 MITTEL: Client-seitiges Data-Fetching mit Wasserfällen im Admin- & Kundenbereich

  • Dateiverweis: src/routes/_admin/admin.index.tsx (Zeilen 2540), src/routes/_admin/admin.users.$userId.tsx (Zeilen 3562)
  • Problem & Architektur-Defizit: Daten werden per useEffect und lokalem useState clientseitig nachgeladen ("Lade Kunden ...").
    • Dies widerspricht dem Konzept von TanStack Start / Router (Loader-first SSR).
    • Führt zu Layout-Shifts, fehlenden SSR-Vorteilen und erschwert Caching, Revalidierung und granulare Fehlergrenzen (CatchBoundary).
  • Lösungsempfehlung: Nutzung von loader in Kombination mit Server Functions (createServerFn):
// src/routes/_admin/admin.index.tsx
export const Route = createFileRoute('/_admin/admin/')({
  loader: async () => await listUsersFn(),
  component: AdminUsersPage,
})

function AdminUsersPage() {
  const users = Route.useLoaderData()
  // Kein useEffect / manual loading state nötig
}

🟡 MITTEL: Fehlende Sicherheits-Header (CSP, HSTS, X-Frame-Options)

  • Dateiverweis: vite.config.ts, src/routes/__root.tsx
  • Problem & Risiko: Es sind keine HTTP-Security-Header konfiguriert. Im __root.tsx wird ein Inline-Script (themeItnitScript) ohne Nonce ausgeführt.
  • Lösungsempfehlung: In Nitro/Vite oder im Webserver (Nginx/Cloudflare) Header setzen:
    • Content-Security-Policy (CSP)
    • X-Frame-Options: DENY (Schutz vor Clickjacking)
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
    • Strict-Transport-Security: max-age=31536000; includeSubDomains

🟢 NIEDRIG: Fehlende Domain-Zentralisierung & Redundante Rollenprüfungen

  • Dateiverweis: src/components/header.tsx (Zeilen 2226), src/routes/login.tsx (Zeilen 10, 47), src/components/app-shell.tsx (Zeile 58)
  • Problem: Die Weiterleitungs- und Rollenlogik user.role === 'admin' ? '/admin' : '/dashboard' ist redundant über mehrere Komponenten verstreut.
  • Lösungsempfehlung: Zentralisierung in einem Auth-Helper, z. B. getDefaultRedirect(session) in src/lib/auth-utils.ts.

2. Konkreter Architektur-Blueprint für Rechnungen & Abnahmen

Um Spaghetti-Code und spätere Umbauten an der Auth-Basis zu vermeiden, sollte die Codebasis jetzt modular strukturiert werden.

A. Empfohlene Feature-basierte Verzeichnisstruktur

src/
├── db/
│   ├── schema/
│   │   ├── auth.ts          # user, session, account, verification
│   │   ├── customers.ts     # customer / tenant Stammdaten
│   │   ├── projects.ts      # Webseiten-Projekte
│   │   ├── invoices.ts      # Rechnungen, Posten, Zahlungsstatus
│   │   └── approvals.ts     # Produktabnahmen, Feedback-Runden
│   └── index.ts             # Drizzle DB Instance & Schema-Export
├── server/
│   ├── middleware/
│   │   ├── auth.ts          # Authentifizierungs-Guard
│   │   └── tenant.ts        # Mandanten-Isolation (Tenant Context)
│   └── services/            # Pure Business Logic (unabhängig von UI)
│       ├── invoice.service.ts
│       └── approval.service.ts
├── routes/
│   ├── _authenticated/
│   │   ├── route.tsx        # Auth Guard & Portal Layout
│   │   ├── dashboard.tsx    # Kunden-Übersicht
│   │   ├── rechnungen/      # Rechnungsmodul (Kunde)
│   │   │   ├── index.tsx
│   │   │   └── $invoiceId.tsx
│   │   └── abnahmen/        # Produktabnahmen (Kunde)
│   │       ├── index.tsx
│   │       └── $approvalId.tsx
│   └── _admin/
│       ├── admin.tsx        # Admin Layout
│       ├── rechnungen/      # Rechnungsverwaltung (Admin)
│       └── kunden/          # Kundenverwaltung

B. Datenmodell-Erweiterung (src/db/schema/)

// 1. Projekte (Klammer um Rechnungen und Abnahmen)
export const project = pgTable('project', {
  id: text('id').primaryKey(),
  customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
  title: text('title').notNull(),
  status: text('status').notNull().default('in_progress'), // 'in_progress' | 'review' | 'live'
  stagingUrl: text('staging_url'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

// 2. Rechnungsmodul
export const invoice = pgTable('invoice', {
  id: text('id').primaryKey(),
  customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
  projectId: text('project_id').references(() => project.id, { onDelete: 'set null' }),
  invoiceNumber: text('invoice_number').notNull().unique(), // z. B. RE-2026-0042
  amountGrossInCents: integer('amount_gross_in_cents').notNull(),
  taxRate: numeric('tax_rate').notNull().default('19.00'),
  status: text('status').notNull().default('open'), // 'open' | 'paid' | 'overdue' | 'cancelled'
  pdfStoragePath: text('pdf_storage_path').notNull(),
  dueDate: timestamp('due_date').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

// 3. Produktabnahme-Modul
export const approval = pgTable('approval', {
  id: text('id').primaryKey(),
  projectId: text('project_id').notNull().references(() => project.id, { onDelete: 'cascade' }),
  customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
  version: text('version').notNull(), // z. B. 'v1.0 - Design-Entwurf'
  status: text('status').notNull().default('pending'), // 'pending' | 'approved' | 'changes_requested'
  feedback: text('feedback'),
  approvedAt: timestamp('approved_at'),
  approvedByUserId: text('approved_by_user_id').references(() => user.id),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

C. Sichere Server-Function-Middleware (Mandanten-Isolation)

Mit TanStack Start Middleware wird sichergestellt, dass jede Abfrage automatisch auf den Mandanten des eingeloggten Benutzers eingeschränkt ist:

// src/server/middleware/tenant.ts
import { createMiddleware } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { auth } from '#/lib/auth.ts'

export const tenantAuthMiddleware = createMiddleware().server(async ({ next }) => {
  const request = getRequest()
  const session = await auth.api.getSession({ headers: request.headers })

  if (!session || !session.user) {
    throw new Error('UNAUTHORIZED')
  }

  return next({
    context: {
      session,
      userId: session.user.id,
      role: session.user.role,
      // Bei Kunden wird die customerId erzwungen; Admins dürfen filtern
      customerId: (session.user as any).customerId as string | null,
      isAdmin: session.user.role === 'admin',
    },
  })
})

Verwendung in Server Functions (IDOR-sicher):

// src/server/functions/invoices.ts
import { createServerFn } from '@tanstack/react-start'
import { tenantAuthMiddleware } from '../middleware/tenant'
import { db } from '#/db/index.ts'
import { invoice } from '#/db/schema.ts'
import { eq, and } from 'drizzle-orm'

export const getInvoicesForCurrentCustomer = createServerFn({ method: 'GET' })
  .middleware([tenantAuthMiddleware])
  .handler(async ({ context }) => {
    if (!context.customerId && !context.isAdmin) {
      return []
    }

    // Automatisch auf den Mandanten des eingeloggten Nutzers begrenzt!
    return await db.query.invoice.findMany({
      where: context.isAdmin
        ? undefined
        : eq(invoice.customerId, context.customerId!),
      orderBy: (inv, { desc }) => [desc(inv.createdAt)],
    })
  })

3. Zusammenfassung des Handlungsbedarfs

Priorität Bereich Maßnahme
Kritisch Architektur Customer/Tenant-Entität im DB-Schema ergänzen & tenantAuthMiddleware etablieren.
Kritisch Sicherheit Devtools in __root.tsx hinter process.env.NODE_ENV !== 'production' kapseln.
Hoch Auth / UX Passwort-Reset-Flow & Einladungs-Link für Kunden einrichten (Klartext-Passwortvergabe im Admin ablösen).
Mittel Performance / Code Admin- und Kunden-Views von useEffect-Fetching auf TanStack Route Loader umstellen.
Mittel Sicherheit HTTP-Sicherheitsheader (CSP, HSTS, X-Frame-Options) konfigurieren.

Mit dieser Vorbereitung ist die Basis optimal gehärtet und Rechnungs- sowie Abnahme-Features können sauber als eigenständige Domain-Module angedockt werden.