Wir haben jetzt einen weitern Stand erreicht.
Hier wäre es angebracht.
Ein Audit durchzuführen.
Prüfe einmal die Sicherheit
Prüfe die Funktionen
Prüfe die Webseite + Datenbank + API
Falls ich was vergessen haben sollte das gut wäre zu prüfen füge es hinzu und schreibe den Bericht als Kommentar.
Sowie eine TO-DO-Liste als extra Kommentar
Wir haben jetzt einen weitern Stand erreicht.
Hier wäre es angebracht.
Ein Audit durchzuführen.
Prüfe einmal die Sicherheit
Prüfe die Funktionen
Prüfe die Webseite + Datenbank + API
Falls ich was vergessen haben sollte das gut wäre zu prüfen füge es hinzu und schreibe den Bericht als Kommentar.
Sowie eine TO-DO-Liste als extra Kommentar
Durchgeführt über drei parallele Prüfungen: automatisierter Security-Code-Review, Funktionalitäts-Review, sowie eine manuelle Live-Prüfung der Infrastruktur (SSH auf den NAS, Docker, Backups, Cron, Live-Site).
Gesamteinschätzung vorab
Solide Basis. Keine CRITICAL-Findings, kein IDOR, keine Injection, keine hartcodierten Secrets, das Ownership-Modell (assertPetOwnership/assertAdmin) wird lückenlos durchgesetzt. Der auffälligste wiederkehrende Punkt: Rate-Limiting ist gebaut, aber nur an 4 von ~20 Routern verdrahtet — das kam unabhängig sowohl aus dem Security- als auch dem Funktionalitäts-Review und ist der Punkt mit dem besten Aufwand/Nutzen-Verhältnis zum Nachziehen.
1. Security
CRITICAL: keine.
HIGH
Rate-Limiting fehlt auf den missbrauchsanfälligsten Endpunkten:messages.send, reports.create, reactions.toggle, follows.create, reposts.create. Nur posts.create, comments.create, explore.listPets, explore.getTrending nutzen die vorhandene rateLimited()-Middleware. Besonders reports.create hat zusätzlich kein Unique-Constraint gegen wiederholtes Melden desselben Posts durch dieselbe Pet-Identität — beides zusammen kann die Moderations-Queue fluten. messages.send ohne Limit ermöglicht DM-Spam/Harassment.
Supabase-Storage-Bucket pet-avatars ohne serverseitige Mime-Type-/Größen-Restriktion: Die Zod-Validierung in posts.ts/media.ts/stories.ts prüft nur, was der Client behauptet. Die signierte Upload-URL selbst bindet weder Content-Type noch Dateigröße. Da der Bucket öffentlich ausgeliefert wird, ist ein hochgeladenes SVG mit eingebettetem Script bei direktem URL-Aufruf ein klassisches Stored-XSS-Muster. Fix ist ein einmaliges Supabase-Dashboard-Setting (fileSizeLimit, allowedMimeTypes), kein Code.
MEDIUM
Mux-Webhook-Signaturprüfung überspringt sich selbst (fail-open), wenn MUX_WEBHOOK_SECRET nicht gesetzt ist — sollte in Produktion hart fehlschlagen statt stillschweigend ungeprüfte Events zu akzeptieren.
Rate-Limiting vertraut X-Forwarded-For/cf-connecting-ip — sicher, solange der Origin-Server ausschließlich Cloudflare-Traffic annimmt (siehe Infra-Punkt unten, das ist aktuell NICHT der Fall).
Admin-/Cron-Secret-Vergleiche nutzen einfaches !== statt crypto.timingSafeEqual — bei einem langen UUID-Secret praktisch kaum ausnutzbar, aber minimaler Aufwand zum Härten.
LOW
Redis ohne Passwort (aktuell nur im Docker-Netzwerk erreichbar, kein Host-Port-Mapping — Defense-in-Depth empfohlen).
search.*, follows.list*, blocks.list ohne Rate-Limit (Scraping-Vektor bei Wachstum).
ads.recordView ohne Dedupe (verfälscht Werbekunden-Reporting, keine Security-Lücke).
Positiv bestätigt: Ownership-Checks konsequent überall, Health-Daten korrekt owner-only (Ausnahme: tokenisierte Health-Card wie spezifiziert), DMs sauber gegen IDOR abgesichert, Admin-Panel doppelt gegated (Pfad-Secret + Rollenprüfung), lückenloses Audit-Log mit IP+Rolle, GDPR-Export/Löschung sauber owner-scoped, keine Injection-Vektoren, keine Secrets im Repo, npm audit 0 Vulnerabilities, Security-Header korrekt gesetzt, Invite-Code-Einlösung rate-limitiert mit Race-Condition-Schutz.
2. Funktionalität
Tests: 136/142 grün, 5 todo (dokumentierte Platzhalter in auth.test.ts für einen nie gebauten Clerk-Webhook-Flow — Owner wird stattdessen korrekt per Lazy-Upsert angelegt, nur der Testkommentar ist veraltet), 1 einmalig geflakter Test (Timeout, bei Isolation/Re-Run zuverlässig grün — kein echter Bug).
Bekannter Admin-Panel-Button-Bug (weißer Hintergrund auf dunklem Panel) ist auf 4 weiteren Seiten noch offen:posts/page.tsx, verification/page.tsx, log/page.tsx ("Load more"-Buttons), sowie invites/page.tsx (Halbfix — nur hover:bg-zinc-800 gesetzt, kein Basis-bg-zinc-900, also im Ruhezustand weiterhin weiß). Bisher nur auf der Users-Seite vollständig gefixt.
3 veraltete TODO-Kommentare (PostCard.tsx, PostTypeSheet.tsx ×2) referenzieren längst gebaute Features (Story/Milestone-Formulare sind fertig verdrahtet) — irreführend für neue Entwickler, sollten gelöscht werden.
15 leere catch-Blöcke bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) — bewusstes "darf Post-Erstellung nie blockieren"-Pattern, aber keiner davon loggt zu GlitchTip. Fiele z. B. Redis aus, würde das niemand bemerken, bis sich Nutzer über leere Feeds beschweren.
i18n-Lücken:src/app/(app)/invite/page.tsx (komplette Seite) und src/components/health/EmergencyVetSection.tsx sind vollständig hartcodiert Deutsch, kein next-intl — für EN-Nutzer aktuell unbenutzbar.
Counter-Konsistenz (Reactions/Comments/Reposts, inkl. Admin-Löschpfade): sauber, keine Lücke gefunden.
3. Website / Infrastruktur / DB / API (live geprüft)
Docker:pawfeed + pawfeed-redis laufen gesund, keine Fehler in den Container-Logs der letzten 24h.
Backups: täglich um 3 Uhr, 14-Tage-Retention, letzter Lauf heute erfolgreich (leere .last-error-Datei).
Disk: 72 % belegt (62 GB frei) — aktuell unkritisch, für die Zukunft im Auge behalten.
SSL: gültig, läuft am 19.09.2026 ab.
Response-Zeit: ~180ms, gesund.
Security-Header: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy alle korrekt gesetzt — aber HSTS wird doppelt gesendet (App setzt max-age=31536000, Cloudflare legt zusätzlich max-age=63072000; preload obendrauf). Browser honorieren nur den ersten Header, funktional unkritisch, aber unsauber — sollte nur an einer Stelle gesetzt werden.
Tokenisierte Routen (Health-Card, GDPR-Export) liefern bei ungültigem Token sauber 404, kein Info-Leak.
Zwei konkrete, neue Infra-Findings:
Der PawFeed-Container ist über Port 4563 direkt und unverschlüsselt (HTTP, nicht HTTPS) im LAN erreichbar, nicht nur über Cloudflare/HTTPS. Bypasst damit Cloudflares WAF/DDoS-Schutz und TLS für jeden, der auf das LAN zugreifen kann. Sollte auf 127.0.0.1/Docker-internes Netzwerk statt 0.0.0.0 gebunden werden, sodass nur Nginx Proxy Manager intern zugreifen kann.
Unauthentifizierte /api/trpc/*-Aufrufe liefern ein HTML-307-Redirect zu Clerks Sign-in-Seite statt eines sauberen JSON-401. Grund: Clerks auth.protect() in proxy.ts fängt die Route ab, bevor der eigentliche tRPC-Handler (der korrekt UNAUTHORIZED werfen würde) überhaupt erreicht wird. Für Browser-Navigation unproblematisch, aber ein API-Client, der eine abgelaufene Session hat, bekommt eine HTML-Antwort statt eines parsebaren Fehlers.
Separate TODO-Liste folgt als eigener Kommentar.
## Audit-Bericht (2026-08-13) — Security, Funktionalität, Website/DB/API
Durchgeführt über drei parallele Prüfungen: automatisierter Security-Code-Review, Funktionalitäts-Review, sowie eine manuelle Live-Prüfung der Infrastruktur (SSH auf den NAS, Docker, Backups, Cron, Live-Site).
### Gesamteinschätzung vorab
Solide Basis. Keine CRITICAL-Findings, kein IDOR, keine Injection, keine hartcodierten Secrets, das Ownership-Modell (`assertPetOwnership`/`assertAdmin`) wird lückenlos durchgesetzt. Der auffälligste wiederkehrende Punkt: **Rate-Limiting ist gebaut, aber nur an 4 von ~20 Routern verdrahtet** — das kam unabhängig sowohl aus dem Security- als auch dem Funktionalitäts-Review und ist der Punkt mit dem besten Aufwand/Nutzen-Verhältnis zum Nachziehen.
---
## 1. Security
**CRITICAL:** keine.
**HIGH**
- **Rate-Limiting fehlt auf den missbrauchsanfälligsten Endpunkten:** `messages.send`, `reports.create`, `reactions.toggle`, `follows.create`, `reposts.create`. Nur `posts.create`, `comments.create`, `explore.listPets`, `explore.getTrending` nutzen die vorhandene `rateLimited()`-Middleware. Besonders `reports.create` hat zusätzlich kein Unique-Constraint gegen wiederholtes Melden desselben Posts durch dieselbe Pet-Identität — beides zusammen kann die Moderations-Queue fluten. `messages.send` ohne Limit ermöglicht DM-Spam/Harassment.
- **Supabase-Storage-Bucket `pet-avatars` ohne serverseitige Mime-Type-/Größen-Restriktion:** Die Zod-Validierung in `posts.ts`/`media.ts`/`stories.ts` prüft nur, was der Client *behauptet*. Die signierte Upload-URL selbst bindet weder Content-Type noch Dateigröße. Da der Bucket öffentlich ausgeliefert wird, ist ein hochgeladenes SVG mit eingebettetem Script bei direktem URL-Aufruf ein klassisches Stored-XSS-Muster. Fix ist ein einmaliges Supabase-Dashboard-Setting (`fileSizeLimit`, `allowedMimeTypes`), kein Code.
**MEDIUM**
- Mux-Webhook-Signaturprüfung überspringt sich selbst (fail-open), wenn `MUX_WEBHOOK_SECRET` nicht gesetzt ist — sollte in Produktion hart fehlschlagen statt stillschweigend ungeprüfte Events zu akzeptieren.
- Rate-Limiting vertraut `X-Forwarded-For`/`cf-connecting-ip` — sicher, solange der Origin-Server ausschließlich Cloudflare-Traffic annimmt (siehe Infra-Punkt unten, das ist aktuell NICHT der Fall).
- Admin-/Cron-Secret-Vergleiche nutzen einfaches `!==` statt `crypto.timingSafeEqual` — bei einem langen UUID-Secret praktisch kaum ausnutzbar, aber minimaler Aufwand zum Härten.
**LOW**
- Redis ohne Passwort (aktuell nur im Docker-Netzwerk erreichbar, kein Host-Port-Mapping — Defense-in-Depth empfohlen).
- `search.*`, `follows.list*`, `blocks.list` ohne Rate-Limit (Scraping-Vektor bei Wachstum).
- `ads.recordView` ohne Dedupe (verfälscht Werbekunden-Reporting, keine Security-Lücke).
**Positiv bestätigt:** Ownership-Checks konsequent überall, Health-Daten korrekt owner-only (Ausnahme: tokenisierte Health-Card wie spezifiziert), DMs sauber gegen IDOR abgesichert, Admin-Panel doppelt gegated (Pfad-Secret + Rollenprüfung), lückenloses Audit-Log mit IP+Rolle, GDPR-Export/Löschung sauber owner-scoped, keine Injection-Vektoren, keine Secrets im Repo, `npm audit` 0 Vulnerabilities, Security-Header korrekt gesetzt, Invite-Code-Einlösung rate-limitiert mit Race-Condition-Schutz.
---
## 2. Funktionalität
- **Tests:** 136/142 grün, 5 `todo` (dokumentierte Platzhalter in `auth.test.ts` für einen nie gebauten Clerk-Webhook-Flow — Owner wird stattdessen korrekt per Lazy-Upsert angelegt, nur der Testkommentar ist veraltet), 1 einmalig geflakter Test (Timeout, bei Isolation/Re-Run zuverlässig grün — kein echter Bug).
- **Bekannter Admin-Panel-Button-Bug (weißer Hintergrund auf dunklem Panel) ist auf 4 weiteren Seiten noch offen:** `posts/page.tsx`, `verification/page.tsx`, `log/page.tsx` ("Load more"-Buttons), sowie `invites/page.tsx` (Halbfix — nur `hover:bg-zinc-800` gesetzt, kein Basis-`bg-zinc-900`, also im Ruhezustand weiterhin weiß). Bisher nur auf der Users-Seite vollständig gefixt.
- **3 veraltete TODO-Kommentare** (`PostCard.tsx`, `PostTypeSheet.tsx` ×2) referenzieren längst gebaute Features (Story/Milestone-Formulare sind fertig verdrahtet) — irreführend für neue Entwickler, sollten gelöscht werden.
- **15 leere `catch`-Blöcke** bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) — bewusstes "darf Post-Erstellung nie blockieren"-Pattern, aber keiner davon loggt zu GlitchTip. Fiele z. B. Redis aus, würde das niemand bemerken, bis sich Nutzer über leere Feeds beschweren.
- **i18n-Lücken:** `src/app/(app)/invite/page.tsx` (komplette Seite) und `src/components/health/EmergencyVetSection.tsx` sind vollständig hartcodiert Deutsch, kein `next-intl` — für EN-Nutzer aktuell unbenutzbar.
- **Counter-Konsistenz** (Reactions/Comments/Reposts, inkl. Admin-Löschpfade): sauber, keine Lücke gefunden.
---
## 3. Website / Infrastruktur / DB / API (live geprüft)
- **Docker:** `pawfeed` + `pawfeed-redis` laufen gesund, keine Fehler in den Container-Logs der letzten 24h.
- **Backups:** täglich um 3 Uhr, 14-Tage-Retention, letzter Lauf heute erfolgreich (leere `.last-error`-Datei).
- **Cron-Jobs** (`trim-feeds`, `anniversaries`): laufen zuverlässig täglich, durchgehend HTTP 200 im Log.
- **Disk:** 72 % belegt (62 GB frei) — aktuell unkritisch, für die Zukunft im Auge behalten.
- **SSL:** gültig, läuft am 19.09.2026 ab.
- **Response-Zeit:** ~180ms, gesund.
- **Security-Header:** HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy alle korrekt gesetzt — **aber HSTS wird doppelt gesendet** (App setzt `max-age=31536000`, Cloudflare legt zusätzlich `max-age=63072000; preload` obendrauf). Browser honorieren nur den ersten Header, funktional unkritisch, aber unsauber — sollte nur an einer Stelle gesetzt werden.
- **Tokenisierte Routen** (Health-Card, GDPR-Export) liefern bei ungültigem Token sauber 404, kein Info-Leak.
- **Zwei konkrete, neue Infra-Findings:**
1. **Der PawFeed-Container ist über Port 4563 direkt und unverschlüsselt (HTTP, nicht HTTPS) im LAN erreichbar**, nicht nur über Cloudflare/HTTPS. Bypasst damit Cloudflares WAF/DDoS-Schutz und TLS für jeden, der auf das LAN zugreifen kann. Sollte auf `127.0.0.1`/Docker-internes Netzwerk statt `0.0.0.0` gebunden werden, sodass nur Nginx Proxy Manager intern zugreifen kann.
2. **Unauthentifizierte `/api/trpc/*`-Aufrufe liefern ein HTML-307-Redirect zu Clerks Sign-in-Seite statt eines sauberen JSON-401.** Grund: Clerks `auth.protect()` in `proxy.ts` fängt die Route ab, bevor der eigentliche tRPC-Handler (der korrekt `UNAUTHORIZED` werfen würde) überhaupt erreicht wird. Für Browser-Navigation unproblematisch, aber ein API-Client, der eine abgelaufene Session hat, bekommt eine HTML-Antwort statt eines parsebaren Fehlers.
---
*Separate TODO-Liste folgt als eigener Kommentar.*
Doppelten HSTS-Header bereinigen (aktuell setzen sowohl die App als auch Cloudflare je einen mit unterschiedlichem max-age)
Mittelfristig
15 leere catch-Blöcke bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) um minimales console.error() ergänzen, damit GlitchTip sie erfasst
i18n vervollständigen: src/app/(app)/invite/page.tsx (komplette Seite) und src/components/health/EmergencyVetSection.tsx sind aktuell komplett hartcodiert Deutsch
crypto.timingSafeEqual für Admin-/Cron-Secret-Vergleiche statt !==
Redis-Passwort setzen (Defense-in-Depth, aktuell nur durch fehlendes Host-Port-Mapping geschützt)
Container-Port 4563 auf 127.0.0.1/Docker-internes Netzwerk statt 0.0.0.0 binden — aktuell direkt unverschlüsselt im LAN erreichbar, umgeht Cloudflare
/api/trpc/* bei fehlender Auth ein sauberes JSON-401 statt HTML-Redirect liefern lassen (Route-Matcher in proxy.ts anpassen)
AUTH-01/AUTH-02-Test-Scaffolds in auth.test.ts (5 todo-Tests) entweder auf das tatsächliche Lazy-Upsert-Pattern umschreiben oder Kommentar korrigieren — beschreiben aktuell eine nie gebaute Webhook-Architektur
Bereits bekannt, bewusst zurückgestellt — nicht neu
CSP scharf schalten (Issue #5, wartet bewusst auf genug GlitchTip-Violation-Daten)
Nur zur Kenntnis, kein Handlungsbedarf
ads.recordView ohne Dedupe — Business-Reporting-Genauigkeit, keine Sicherheitslücke
search.*/follows.list*/blocks.list ohne Rate-Limit — niedriges Risiko bei aktueller Nutzerzahl, für später vormerken
## TODO-Liste aus dem Audit (priorisiert)
### Kurzfristig — geringer Aufwand, hoher Nutzen
- [ ] Rate-Limiting ergänzen auf `messages.send`, `reports.create`, `reactions.toggle`, `follows.create`, `reposts.create` (Vorlage: `posts.create`/`comments.create`, `rateLimited()`-Middleware existiert bereits)
- [ ] Unique-Constraint gegen wiederholtes Melden desselben Posts durch dieselbe Pet-Identität in `reports.create`
- [ ] Supabase-Dashboard: `pet-avatars`-Bucket mit `fileSizeLimit` + `allowedMimeTypes` einschränken (kein Code, nur Setting)
- [ ] Mux-Webhook: in Produktion hart fehlschlagen, wenn `MUX_WEBHOOK_SECRET` fehlt, statt Signaturprüfung zu überspringen
- [ ] Admin-Panel Outline-Button-Fix (`bg-zinc-900 hover:bg-zinc-800`) auf 4 weitere Seiten nachziehen: `posts/page.tsx`, `verification/page.tsx`, `log/page.tsx`, `invites/page.tsx` (dort nur Hover gefixt, Basis-Hintergrund fehlt noch)
- [ ] 3 veralteten TODO-Kommentare löschen (`PostCard.tsx:57`, `PostTypeSheet.tsx:52,55`) — referenzieren längst gebaute Features
- [ ] Doppelten HSTS-Header bereinigen (aktuell setzen sowohl die App als auch Cloudflare je einen mit unterschiedlichem `max-age`)
### Mittelfristig
- [ ] 15 leere `catch`-Blöcke bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) um minimales `console.error()` ergänzen, damit GlitchTip sie erfasst
- [ ] i18n vervollständigen: `src/app/(app)/invite/page.tsx` (komplette Seite) und `src/components/health/EmergencyVetSection.tsx` sind aktuell komplett hartcodiert Deutsch
- [ ] `crypto.timingSafeEqual` für Admin-/Cron-Secret-Vergleiche statt `!==`
- [ ] Redis-Passwort setzen (Defense-in-Depth, aktuell nur durch fehlendes Host-Port-Mapping geschützt)
- [ ] Container-Port 4563 auf `127.0.0.1`/Docker-internes Netzwerk statt `0.0.0.0` binden — aktuell direkt unverschlüsselt im LAN erreichbar, umgeht Cloudflare
- [ ] `/api/trpc/*` bei fehlender Auth ein sauberes JSON-401 statt HTML-Redirect liefern lassen (Route-Matcher in `proxy.ts` anpassen)
- [ ] `AUTH-01`/`AUTH-02`-Test-Scaffolds in `auth.test.ts` (5 `todo`-Tests) entweder auf das tatsächliche Lazy-Upsert-Pattern umschreiben oder Kommentar korrigieren — beschreiben aktuell eine nie gebaute Webhook-Architektur
### Bereits bekannt, bewusst zurückgestellt — nicht neu
- CSP scharf schalten (Issue #5, wartet bewusst auf genug GlitchTip-Violation-Daten)
### Nur zur Kenntnis, kein Handlungsbedarf
- `ads.recordView` ohne Dedupe — Business-Reporting-Genauigkeit, keine Sicherheitslücke
- `search.*`/`follows.list*`/`blocks.list` ohne Rate-Limit — niedriges Risiko bei aktueller Nutzerzahl, für später vormerken
Autonom umgesetzt (11 von 12 Punkten, Commits b4a49f8 + ebc5267, deployed + live verifiziert)
Alle Punkte außer der Port-4563-Bindung sind erledigt — die habe ich bewusst zurückgestellt, weil sie mit der extern (GUI) verwalteten Nginx-Proxy-Manager-Konfiguration koordiniert werden müsste und im Fehlerfall die Seite von außen unerreichbar machen könnte.
Erledigt
Rate-Limiting auf messages.send, reports.create, reactions.toggle, follows.create, reposts.create
Unique-Constraints gegen Report-Spam ((reporterPetId, targetPostId) + (reporterPetId, targetPetId)) — mit explizitem DB-Consent umgesetzt, Prismas eigener KI-Sicherheitsmechanismus hat dafür extra nachgefragt
Supabase pet-avatars-Bucket: fileSizeLimit (10 MB) + allowedMimeTypes (jpeg/png/webp) gesetzt — entspricht exakt den bereits im Code etablierten Konstanten
Mux-Webhook: fail-hard (500) in Produktion ohne gesetztes Secret statt Signaturprüfung zu überspringen
Admin-Panel Outline-Button-Fix auf posts, verification, log, invites nachgezogen
3 veraltete TODO-Kommentare gelöscht
Doppelter HSTS-Header behoben (App setzte einen schwächeren zusätzlich zu Cloudflares)
crypto-freier Constant-Time-Vergleich für ADMIN_SECRET/CRON_SECRET (bewusst ohne node:crypto, da proxy.ts potenziell im Edge-Runtime läuft)
14 vormals leere catch-Blöcke loggen jetzt zu GlitchTip
/api/trpc/* liefert bei fehlender Auth jetzt sauberes JSON-401 statt HTML-Redirect — als Nebeneffekt war das vorher auch für publicProcedure-Aufrufe (z. B. explore.listPets) für ausgeloggte Besucher kaputt, ist jetzt mit gefixt
auth.test.ts-Scaffold korrigiert: AUTH-01 ist bereits real getestet (in pet-profile.test.ts), AUTH-02 ehrlich als bekannte, unimplementierte Lücke dokumentiert statt eine nie gebaute Webhook-Architektur zu beschreiben
Redis-Passwort gesetzt (--requirepass + REDIS_URL entsprechend angepasst), live verifiziert: ohne Passwort NOAUTH, mit Passwort PONG
Zurückgestellt — braucht Abstimmung
Container-Port 4563 auf 127.0.0.1/Docker-internes Netzwerk binden statt 0.0.0.0 — würde die Nginx-Proxy-Manager-Weiterleitung (172.17.0.1:4563) betreffen, dort müsste der Forward-Zielwert ggf. mitgeändert werden. Bei Bedarf bitte kurz Bescheid geben, dann gehe ich das zusammen mit dir an.
tsc/Vitest (137/138, 1 bewusst offener todo)/Docker-Build bei jedem Schritt sauber, Live-Site nach jedem Deploy auf HTTP 200 verifiziert.
## Autonom umgesetzt (11 von 12 Punkten, Commits `b4a49f8` + `ebc5267`, deployed + live verifiziert)
Alle Punkte außer der Port-4563-Bindung sind erledigt — die habe ich bewusst zurückgestellt, weil sie mit der extern (GUI) verwalteten Nginx-Proxy-Manager-Konfiguration koordiniert werden müsste und im Fehlerfall die Seite von außen unerreichbar machen könnte.
### Erledigt
- [x] Rate-Limiting auf `messages.send`, `reports.create`, `reactions.toggle`, `follows.create`, `reposts.create`
- [x] Unique-Constraints gegen Report-Spam (`(reporterPetId, targetPostId)` + `(reporterPetId, targetPetId)`) — mit explizitem DB-Consent umgesetzt, Prismas eigener KI-Sicherheitsmechanismus hat dafür extra nachgefragt
- [x] Supabase `pet-avatars`-Bucket: `fileSizeLimit` (10 MB) + `allowedMimeTypes` (jpeg/png/webp) gesetzt — entspricht exakt den bereits im Code etablierten Konstanten
- [x] Mux-Webhook: fail-hard (500) in Produktion ohne gesetztes Secret statt Signaturprüfung zu überspringen
- [x] Admin-Panel Outline-Button-Fix auf `posts`, `verification`, `log`, `invites` nachgezogen
- [x] 3 veraltete TODO-Kommentare gelöscht
- [x] Doppelter HSTS-Header behoben (App setzte einen schwächeren zusätzlich zu Cloudflares)
- [x] `crypto`-freier Constant-Time-Vergleich für `ADMIN_SECRET`/`CRON_SECRET` (bewusst ohne `node:crypto`, da `proxy.ts` potenziell im Edge-Runtime läuft)
- [x] 14 vormals leere `catch`-Blöcke loggen jetzt zu GlitchTip
- [x] `invite/page.tsx` + `EmergencyVetSection.tsx` vollständig lokalisiert (waren 100 % hartcodiert Deutsch)
- [x] `/api/trpc/*` liefert bei fehlender Auth jetzt sauberes JSON-401 statt HTML-Redirect — als Nebeneffekt war das vorher auch für `publicProcedure`-Aufrufe (z. B. `explore.listPets`) für ausgeloggte Besucher kaputt, ist jetzt mit gefixt
- [x] `auth.test.ts`-Scaffold korrigiert: AUTH-01 ist bereits real getestet (in `pet-profile.test.ts`), AUTH-02 ehrlich als bekannte, unimplementierte Lücke dokumentiert statt eine nie gebaute Webhook-Architektur zu beschreiben
- [x] Redis-Passwort gesetzt (`--requirepass` + `REDIS_URL` entsprechend angepasst), live verifiziert: ohne Passwort `NOAUTH`, mit Passwort `PONG`
### Zurückgestellt — braucht Abstimmung
- [ ] Container-Port 4563 auf `127.0.0.1`/Docker-internes Netzwerk binden statt `0.0.0.0` — würde die Nginx-Proxy-Manager-Weiterleitung (`172.17.0.1:4563`) betreffen, dort müsste der Forward-Zielwert ggf. mitgeändert werden. Bei Bedarf bitte kurz Bescheid geben, dann gehe ich das zusammen mit dir an.
tsc/Vitest (137/138, 1 bewusst offener `todo`)/Docker-Build bei jedem Schritt sauber, Live-Site nach jedem Deploy auf HTTP 200 verifiziert.
Verifiziert: Live-Site läuft weiter normal (https://pawfeed.org → 200, tRPC-API antwortet korrekt), direkter Zugriff auf 192.168.1.222:4563 schlägt jetzt fehl (Connection refused), docker ps zeigt für den pawfeed-Container nur noch den internen Port 3000/tcp, kein Host-Mapping mehr. Keine Downtime während der Umstellung (zweistufiges Vorgehen: erst Netzwerk+Verifikation, dann erst Port-Entfernung).
Damit ist der Container nur noch über Nginx Proxy Manager → Cloudflare erreichbar, nicht mehr direkt unverschlüsselt im LAN.
Alle 12 Audit-Punkte sind damit abgeschlossen. Schließe das Issue.
## Letzter Punkt erledigt: Port-4563-Bindung (Commits `9885fc9` + `2d0e390`, deployed)
Zusammen mit dir umgesetzt (brauchte Zugriff auf die Nginx-Proxy-Manager-Oberfläche, die ich nicht selbstständig hätte anfassen sollen):
1. PawFeed dem `nginx_default`-Docker-Netzwerk hinzugefügt (gleiches Netzwerk wie NPM), Host-Port-Mapping zunächst noch behalten
2. Erreichbarkeit von `pawfeed:3000` aus dem NPM-Container heraus verifiziert (HTTP 200)
3. NPM-Proxy-Host für `pawfeed.org`/`www.pawfeed.org`/`pawfeed.neodk.ipv64.de` von `http://192.168.1.222:4563` auf `http://pawfeed:3000` umgestellt
4. Live-Site über die echte Domain mehrfach verifiziert (200, ~200-370ms)
5. Host-Port-Mapping `4563:3000` komplett entfernt, redeployed
**Verifiziert:** Live-Site läuft weiter normal (`https://pawfeed.org` → 200, tRPC-API antwortet korrekt), direkter Zugriff auf `192.168.1.222:4563` schlägt jetzt fehl (Connection refused), `docker ps` zeigt für den `pawfeed`-Container nur noch den internen Port `3000/tcp`, kein Host-Mapping mehr. Keine Downtime während der Umstellung (zweistufiges Vorgehen: erst Netzwerk+Verifikation, dann erst Port-Entfernung).
Damit ist der Container nur noch über Nginx Proxy Manager → Cloudflare erreichbar, nicht mehr direkt unverschlüsselt im LAN.
---
**Alle 12 Audit-Punkte sind damit abgeschlossen.** Schließe das Issue.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Wir haben jetzt einen weitern Stand erreicht.
Hier wäre es angebracht.
Ein Audit durchzuführen.
Prüfe einmal die Sicherheit
Prüfe die Funktionen
Prüfe die Webseite + Datenbank + API
Falls ich was vergessen haben sollte das gut wäre zu prüfen füge es hinzu und schreibe den Bericht als Kommentar.
Sowie eine TO-DO-Liste als extra Kommentar
Audit-Bericht (2026-08-13) — Security, Funktionalität, Website/DB/API
Durchgeführt über drei parallele Prüfungen: automatisierter Security-Code-Review, Funktionalitäts-Review, sowie eine manuelle Live-Prüfung der Infrastruktur (SSH auf den NAS, Docker, Backups, Cron, Live-Site).
Gesamteinschätzung vorab
Solide Basis. Keine CRITICAL-Findings, kein IDOR, keine Injection, keine hartcodierten Secrets, das Ownership-Modell (
assertPetOwnership/assertAdmin) wird lückenlos durchgesetzt. Der auffälligste wiederkehrende Punkt: Rate-Limiting ist gebaut, aber nur an 4 von ~20 Routern verdrahtet — das kam unabhängig sowohl aus dem Security- als auch dem Funktionalitäts-Review und ist der Punkt mit dem besten Aufwand/Nutzen-Verhältnis zum Nachziehen.1. Security
CRITICAL: keine.
HIGH
messages.send,reports.create,reactions.toggle,follows.create,reposts.create. Nurposts.create,comments.create,explore.listPets,explore.getTrendingnutzen die vorhandenerateLimited()-Middleware. Besondersreports.createhat zusätzlich kein Unique-Constraint gegen wiederholtes Melden desselben Posts durch dieselbe Pet-Identität — beides zusammen kann die Moderations-Queue fluten.messages.sendohne Limit ermöglicht DM-Spam/Harassment.pet-avatarsohne serverseitige Mime-Type-/Größen-Restriktion: Die Zod-Validierung inposts.ts/media.ts/stories.tsprüft nur, was der Client behauptet. Die signierte Upload-URL selbst bindet weder Content-Type noch Dateigröße. Da der Bucket öffentlich ausgeliefert wird, ist ein hochgeladenes SVG mit eingebettetem Script bei direktem URL-Aufruf ein klassisches Stored-XSS-Muster. Fix ist ein einmaliges Supabase-Dashboard-Setting (fileSizeLimit,allowedMimeTypes), kein Code.MEDIUM
MUX_WEBHOOK_SECRETnicht gesetzt ist — sollte in Produktion hart fehlschlagen statt stillschweigend ungeprüfte Events zu akzeptieren.X-Forwarded-For/cf-connecting-ip— sicher, solange der Origin-Server ausschließlich Cloudflare-Traffic annimmt (siehe Infra-Punkt unten, das ist aktuell NICHT der Fall).!==stattcrypto.timingSafeEqual— bei einem langen UUID-Secret praktisch kaum ausnutzbar, aber minimaler Aufwand zum Härten.LOW
search.*,follows.list*,blocks.listohne Rate-Limit (Scraping-Vektor bei Wachstum).ads.recordViewohne Dedupe (verfälscht Werbekunden-Reporting, keine Security-Lücke).Positiv bestätigt: Ownership-Checks konsequent überall, Health-Daten korrekt owner-only (Ausnahme: tokenisierte Health-Card wie spezifiziert), DMs sauber gegen IDOR abgesichert, Admin-Panel doppelt gegated (Pfad-Secret + Rollenprüfung), lückenloses Audit-Log mit IP+Rolle, GDPR-Export/Löschung sauber owner-scoped, keine Injection-Vektoren, keine Secrets im Repo,
npm audit0 Vulnerabilities, Security-Header korrekt gesetzt, Invite-Code-Einlösung rate-limitiert mit Race-Condition-Schutz.2. Funktionalität
todo(dokumentierte Platzhalter inauth.test.tsfür einen nie gebauten Clerk-Webhook-Flow — Owner wird stattdessen korrekt per Lazy-Upsert angelegt, nur der Testkommentar ist veraltet), 1 einmalig geflakter Test (Timeout, bei Isolation/Re-Run zuverlässig grün — kein echter Bug).posts/page.tsx,verification/page.tsx,log/page.tsx("Load more"-Buttons), sowieinvites/page.tsx(Halbfix — nurhover:bg-zinc-800gesetzt, kein Basis-bg-zinc-900, also im Ruhezustand weiterhin weiß). Bisher nur auf der Users-Seite vollständig gefixt.PostCard.tsx,PostTypeSheet.tsx×2) referenzieren längst gebaute Features (Story/Milestone-Formulare sind fertig verdrahtet) — irreführend für neue Entwickler, sollten gelöscht werden.catch-Blöcke bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) — bewusstes "darf Post-Erstellung nie blockieren"-Pattern, aber keiner davon loggt zu GlitchTip. Fiele z. B. Redis aus, würde das niemand bemerken, bis sich Nutzer über leere Feeds beschweren.src/app/(app)/invite/page.tsx(komplette Seite) undsrc/components/health/EmergencyVetSection.tsxsind vollständig hartcodiert Deutsch, keinnext-intl— für EN-Nutzer aktuell unbenutzbar.3. Website / Infrastruktur / DB / API (live geprüft)
pawfeed+pawfeed-redislaufen gesund, keine Fehler in den Container-Logs der letzten 24h..last-error-Datei).trim-feeds,anniversaries): laufen zuverlässig täglich, durchgehend HTTP 200 im Log.max-age=31536000, Cloudflare legt zusätzlichmax-age=63072000; preloadobendrauf). Browser honorieren nur den ersten Header, funktional unkritisch, aber unsauber — sollte nur an einer Stelle gesetzt werden.127.0.0.1/Docker-internes Netzwerk statt0.0.0.0gebunden werden, sodass nur Nginx Proxy Manager intern zugreifen kann./api/trpc/*-Aufrufe liefern ein HTML-307-Redirect zu Clerks Sign-in-Seite statt eines sauberen JSON-401. Grund: Clerksauth.protect()inproxy.tsfängt die Route ab, bevor der eigentliche tRPC-Handler (der korrektUNAUTHORIZEDwerfen würde) überhaupt erreicht wird. Für Browser-Navigation unproblematisch, aber ein API-Client, der eine abgelaufene Session hat, bekommt eine HTML-Antwort statt eines parsebaren Fehlers.Separate TODO-Liste folgt als eigener Kommentar.
TODO-Liste aus dem Audit (priorisiert)
Kurzfristig — geringer Aufwand, hoher Nutzen
messages.send,reports.create,reactions.toggle,follows.create,reposts.create(Vorlage:posts.create/comments.create,rateLimited()-Middleware existiert bereits)reports.createpet-avatars-Bucket mitfileSizeLimit+allowedMimeTypeseinschränken (kein Code, nur Setting)MUX_WEBHOOK_SECRETfehlt, statt Signaturprüfung zu überspringenbg-zinc-900 hover:bg-zinc-800) auf 4 weitere Seiten nachziehen:posts/page.tsx,verification/page.tsx,log/page.tsx,invites/page.tsx(dort nur Hover gefixt, Basis-Hintergrund fehlt noch)PostCard.tsx:57,PostTypeSheet.tsx:52,55) — referenzieren längst gebaute Featuresmax-age)Mittelfristig
catch-Blöcke bei Nebeneffekten (Redis-Fan-out, Hashtag-Indexierung, Mention-Notifications) um minimalesconsole.error()ergänzen, damit GlitchTip sie erfasstsrc/app/(app)/invite/page.tsx(komplette Seite) undsrc/components/health/EmergencyVetSection.tsxsind aktuell komplett hartcodiert Deutschcrypto.timingSafeEqualfür Admin-/Cron-Secret-Vergleiche statt!==127.0.0.1/Docker-internes Netzwerk statt0.0.0.0binden — aktuell direkt unverschlüsselt im LAN erreichbar, umgeht Cloudflare/api/trpc/*bei fehlender Auth ein sauberes JSON-401 statt HTML-Redirect liefern lassen (Route-Matcher inproxy.tsanpassen)AUTH-01/AUTH-02-Test-Scaffolds inauth.test.ts(5todo-Tests) entweder auf das tatsächliche Lazy-Upsert-Pattern umschreiben oder Kommentar korrigieren — beschreiben aktuell eine nie gebaute Webhook-ArchitekturBereits bekannt, bewusst zurückgestellt — nicht neu
Nur zur Kenntnis, kein Handlungsbedarf
ads.recordViewohne Dedupe — Business-Reporting-Genauigkeit, keine Sicherheitslückesearch.*/follows.list*/blocks.listohne Rate-Limit — niedriges Risiko bei aktueller Nutzerzahl, für später vormerkenAutonom umgesetzt (11 von 12 Punkten, Commits
b4a49f8+ebc5267, deployed + live verifiziert)Alle Punkte außer der Port-4563-Bindung sind erledigt — die habe ich bewusst zurückgestellt, weil sie mit der extern (GUI) verwalteten Nginx-Proxy-Manager-Konfiguration koordiniert werden müsste und im Fehlerfall die Seite von außen unerreichbar machen könnte.
Erledigt
messages.send,reports.create,reactions.toggle,follows.create,reposts.create(reporterPetId, targetPostId)+(reporterPetId, targetPetId)) — mit explizitem DB-Consent umgesetzt, Prismas eigener KI-Sicherheitsmechanismus hat dafür extra nachgefragtpet-avatars-Bucket:fileSizeLimit(10 MB) +allowedMimeTypes(jpeg/png/webp) gesetzt — entspricht exakt den bereits im Code etablierten Konstantenposts,verification,log,invitesnachgezogencrypto-freier Constant-Time-Vergleich fürADMIN_SECRET/CRON_SECRET(bewusst ohnenode:crypto, daproxy.tspotenziell im Edge-Runtime läuft)catch-Blöcke loggen jetzt zu GlitchTipinvite/page.tsx+EmergencyVetSection.tsxvollständig lokalisiert (waren 100 % hartcodiert Deutsch)/api/trpc/*liefert bei fehlender Auth jetzt sauberes JSON-401 statt HTML-Redirect — als Nebeneffekt war das vorher auch fürpublicProcedure-Aufrufe (z. B.explore.listPets) für ausgeloggte Besucher kaputt, ist jetzt mit gefixtauth.test.ts-Scaffold korrigiert: AUTH-01 ist bereits real getestet (inpet-profile.test.ts), AUTH-02 ehrlich als bekannte, unimplementierte Lücke dokumentiert statt eine nie gebaute Webhook-Architektur zu beschreiben--requirepass+REDIS_URLentsprechend angepasst), live verifiziert: ohne PasswortNOAUTH, mit PasswortPONGZurückgestellt — braucht Abstimmung
127.0.0.1/Docker-internes Netzwerk binden statt0.0.0.0— würde die Nginx-Proxy-Manager-Weiterleitung (172.17.0.1:4563) betreffen, dort müsste der Forward-Zielwert ggf. mitgeändert werden. Bei Bedarf bitte kurz Bescheid geben, dann gehe ich das zusammen mit dir an.tsc/Vitest (137/138, 1 bewusst offener
todo)/Docker-Build bei jedem Schritt sauber, Live-Site nach jedem Deploy auf HTTP 200 verifiziert.Letzter Punkt erledigt: Port-4563-Bindung (Commits
9885fc9+2d0e390, deployed)Zusammen mit dir umgesetzt (brauchte Zugriff auf die Nginx-Proxy-Manager-Oberfläche, die ich nicht selbstständig hätte anfassen sollen):
nginx_default-Docker-Netzwerk hinzugefügt (gleiches Netzwerk wie NPM), Host-Port-Mapping zunächst noch behaltenpawfeed:3000aus dem NPM-Container heraus verifiziert (HTTP 200)pawfeed.org/www.pawfeed.org/pawfeed.neodk.ipv64.devonhttp://192.168.1.222:4563aufhttp://pawfeed:3000umgestellt4563:3000komplett entfernt, redeployedVerifiziert: Live-Site läuft weiter normal (
https://pawfeed.org→ 200, tRPC-API antwortet korrekt), direkter Zugriff auf192.168.1.222:4563schlägt jetzt fehl (Connection refused),docker pszeigt für denpawfeed-Container nur noch den internen Port3000/tcp, kein Host-Mapping mehr. Keine Downtime während der Umstellung (zweistufiges Vorgehen: erst Netzwerk+Verifikation, dann erst Port-Entfernung).Damit ist der Container nur noch über Nginx Proxy Manager → Cloudflare erreichbar, nicht mehr direkt unverschlüsselt im LAN.
Alle 12 Audit-Punkte sind damit abgeschlossen. Schließe das Issue.