jagd-apps/AUDIT_REPORT_2026.md

29 KiB
Raw Blame History

Sicherheits- und Architektur-Audit Jagd-Ökosystem

Datum: 3. Mai 2026
Scope: Vollständiges Ökosystem (Portal + 3 Applikationen)
Modus: Read-Only keine Code-Änderungen vorgenommen
Prüfer: GitHub Copilot (Claude Sonnet 4.6)


Inhaltsverzeichnis

  1. Executive Summary
  2. Architektur-Übersicht
  3. Befund-Register
  4. System-spezifische Befunde
  5. Positive Befunde
  6. Priorisierte Handlungsempfehlungen
  7. Technischer Anhang

1. Executive Summary

Das geprüfte Ökosystem besteht aus einem zentralen Nginx-Portal und drei voneinander getrennten Webanwendungen (Nachsuche, Drohnenfuehrer, Stoeberhunde) für die Koordination von Jagdaktivitäten. Die Anwendungen teilen eine identische technische Basis (Node.js/Express, MongoDB, React/Vite).

Gesamtbewertung: HOCH-RISIKOREICH

Es wurden 2 kritische, 7 hohe, 8 mittlere und 7 niedrige Befunde identifiziert. Zwei der kritischen Befunde ermöglichen entweder eine unbeabsichtigte Rechteausweitung über App-Grenzen hinweg oder machen einen produktiven Endpunkt vollständig funktionsunfähig.

Priorität Anzahl Sofortmaßnahme erforderlich
KRITISCH 2 Ja
HOCH 7 Ja
MITTEL 8 Nein (innerhalb 30 Tage)
NIEDRIG 7 Nein (innerhalb 90 Tage)
Positiv 12

2. Architektur-Übersicht

2.1 Container-Topologie

Internet
    │
    ▼
[Portal nginx :8090]          (jagd-portal Container)
    │
    ├── /nachsuche/api/     → nachsuche-backend:5000
    ├── /nachsuche/         → nachsuche-frontend:80
    ├── /drohnenfuehrer/api/ → drohnenfuehrer-backend:5000
    ├── /drohnenfuehrer/    → drohnenfuehrer-frontend:80
    ├── /stoeberhunde/api/  → stoeberhunde-backend:5000
    └── /stoeberhunde/      → stoeberhunde-frontend:80
            │
            └── alle Container in: jagd-network (externes Docker-Netzwerk)

2.2 Port-Belegung

Container Interner Port Host-Binding
nachsuche-backend 5000 127.0.0.1:5010
nachsuche-frontend 80 127.0.0.1:8080
drohnenfuehrer-backend 5000 127.0.0.1:5011
drohnenfuehrer-frontend 80 127.0.0.1:8081
stoeberhunde-backend 5000 127.0.0.1:5012
stoeberhunde-frontend 80 127.0.0.1:8082

Backend-Ports sind korrekt auf 127.0.0.1 gebunden (nicht extern erreichbar). MongoDB-Instanzen sind nicht nach außen exponiert.

2.3 Rollen-Modell

Admin (Cookie, httpOnly JWT)
  └── Zugriff auf: User-CRUD, Config, Audit-Log, Invite-Token-Generierung

Handler / Drohnenführer / Stöberhundeführer (Bearer JWT, localStorage)
  └── Zugriff auf: eigenes Profil (/me), Verfügbarkeit setzen

Öffentlich (kein Token)
  └── GET /api/public/users → name, type, available, gps

3. Befund-Register

3.1 Kritische Befunde


C-01 — Implizite Cross-App-Admin-Authentifizierung

Betroffen: Alle drei Apps
Dateien: {app}/backend/config/env.js, {app}/backend/middleware/auth.js, {app}/backend/controllers/authController.js

Beschreibung:
Alle drei Apps lesen ihren JWT-Signing-Key aus der identisch benannten Umgebungsvariable JWT_SECRET. In der typischen Deployment-Konfiguration wird diese Variable für alle Apps auf denselben Wert gesetzt (oder aus derselben .env-Datei bezogen). Das Admin-Cookie wird gesetzt mit:

// authController.js (alle Apps)
res.cookie('token', token, {
  httpOnly: true,
  secure: ...,
  sameSite: 'lax',
  path: '/'      // ← gilt für ALLE Pfade auf der Domain
});

Da das Portal eine einzige Domain (z. B. jagd.example.org) bedient und alle drei Apps unter Subpfaden laufen (/nachsuche/, /drohnenfuehrer/, /stoeberhunde/), sendet der Browser das Admin-Cookie automatisch an alle drei App-Backends. Jeder Backend-authenticateToken-Middleware nimmt ein gültiges Cookie an, das von irgendeiner der drei Apps ausgestellt wurde, da alle denselben Secret kennen.

Auswirkung: Ein Admin-Login bei Nachsuche gewährt automatisch Admin-Zugriff auf Drohnenfuehrer und Stoeberhunde ohne weiteren Login. Umgekehrt gilt dasselbe.

Empfehlung:

  1. Jede App erhält einen eigenen, einzigartigen JWT_SECRET (z. B. NACHSUCHE_JWT_SECRET, DROHNENFUEHRER_JWT_SECRET, etc.)
  2. Das JWT-Payload erhält einen App-spezifischen Claim: { id, username, app: 'nachsuche' }
  3. authenticateToken prüft: if (decoded.app !== 'nachsuche') return res.status(403)

C-02 — ReferenceError in importUsers(): Variable overwrite nie deklariert

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js, Zeile ~494

Beschreibung:
Die Funktion importUsers iteriert über eine Liste zu importierender Benutzer und soll bei ?overwrite=true bestehende Einträge überschreiben. Die Variable overwrite wird jedoch nirgends deklariert:

const importUsers = async (req, res) => {
  // ...kein: const overwrite = req.query.overwrite === 'true';
  for (const entry of importList) {
    const existing = await User.findOne(...);
    if (existing && !overwrite) {   // ReferenceError: overwrite is not defined

Der Fehler tritt auf, sobald der erste Benutzer in der Import-Liste bereits in der Datenbank existiert. Der innere try/catch fängt den Fehler ab, loggt ihn als Einzel-Error, und der gesamte Import-Vorgang schlägt für alle nachfolgenden Einträge still fehl.

Auswirkung: POST /api/users/import mit einer bereits vorhandenen Person in der Liste crasht intern. Der Import-Endpunkt ist in der Praxis nicht nutzbar mit vorhandenen Daten.

Fix:

const importUsers = async (req, res) => {
  const { users: importList } = req.body;
  const overwrite = req.query.overwrite === 'true';  // ← diese Zeile fehlt
  // ...

3.2 Hohe Befunde


H-01 — passwordHash in Admin-API-Antworten exponiert (getAllUsers)

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js, getAllUsers, Zeile ~32

Beschreibung:

User.find(filter)
  .sort(...)
  .skip(skip)
  .limit(limit)
  .select('-__v')   // ← schließt NUR __v aus, nicht passwordHash

Das User-Mongoose-Schema definiert passwordHash ohne select: false. Damit ist der bcrypt-Hash in jeder Admin-Benutzerlistanfrage enthalten.

Auswirkung: Ein kompromittierter Admin-Account oder eine Session-Hijacking-Lücke gibt Angreifern alle bcrypt-Hashes der Nutzer, die für Offline-Brute-Force-Angriffe genutzt werden können.

Fix: .select('-__v -passwordHash') in getAllUsers und getUserById.


H-02 — passwordHash in getUserById() exponiert (kein Select)

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js, getUserById, Zeile ~57

Beschreibung:

const user = await User.findById(req.params.id);  // ← keinerlei .select()

Das vollständige Mongoose-Dokument wird zurückgegeben, inklusive passwordHash. Alternativ sollte das Schema-Feld mit select: false versehen werden.


H-03 — Mass Assignment in createUser() und updateUser()

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js

Beschreibung:

// createUser
const payload = { ...req.body };        // ← gesamter Request-Body
const user = new User(payload);

// updateUser
const updateData = { ...req.body };     // ← gesamter Request-Body
await User.findByIdAndUpdate(req.params.id, updateData, ...);

Ein Angreifer mit Admin-Zugriff (oder bei einer zukünftigen BOLA-Lücke) kann beliebige Schema-Felder setzen, darunter:

  • deleted: false Soft-Delete umgehen
  • passwordHash: "..." direkt einen bekannten Hash injizieren
  • inviteToken / inviteTokenExpiry Einladungstoken manipulieren
  • deletedBy, deletedAt Audit-Trail fälschen

Fix: Whitelist der erlaubten Felder explizit definieren, analog zu bulkUpdateUsers:

const ALLOWED_CREATE_FIELDS = ['name', 'type', 'address', 'phone', 'landline', 'email', 'available', 'gps', 'notes'];
const payload = Object.fromEntries(
  Object.entries(req.body).filter(([k]) => ALLOWED_CREATE_FIELDS.includes(k))
);

H-04 — Set-Password-Flow komplett defekt: inviteToken fehlt im Frontend

Betroffen: Alle drei Apps
Dateien: nachsuche/frontend/src/services/handler.js, drohnenfuehrer/frontend/src/services/drohnenfuehrer.js, stoeberhunde/frontend/src/services/stoeberhundefuehrer.js

Beschreibung:
Das Backend erwartet bei POST /api/{rolle}/set-password drei Parameter:

{ email, inviteToken, newPassword }

Das Frontend sendet:

drohnenfuehrerApi.post('/set-password', { email, newPassword });  // inviteToken fehlt

Das Backend antwortet immer mit HTTP 400: "E-Mail, Einladungs-Token und Passwort (min. 8 Zeichen) erforderlich".

Auswirkung: Neu angelegte Handler, Drohnenführer und Stöberhundeführer können sich niemals anmelden. Der Admin kann zwar einen Invite-Token generieren, aber Nutzer können ihn nicht im Frontend eingeben. Das Onboarding neuer Nutzer ist vollständig blockiert.

Fix: Im Passwort-Setzen-Formular ein Eingabefeld für den Invite-Token hinzufügen und diesen im API-Aufruf mitsenden.


Betroffen: Stoeberhunde
Datei: stoeberhunde/backend/controllers/authController.js, Zeile ~75

Beschreibung:

res.cookie('token', token, {
  httpOnly: true,
  secure: false,      // ← Hardcoded, nie HTTPS-Only
  sameSite: 'lax',
  path: '/'
});

Im Vergleich dazu setzen Nachsuche und Drohnenfuehrer:

const secureCookie = process.env.NODE_ENV === 'production';
secure: secureCookie   // ← korrekt: HTTPS in Production erzwungen

Auswirkung: Das Admin-Auth-Cookie wird auch über unverschlüsselte HTTP-Verbindungen übertragen, selbst in der Produktionsumgebung.


H-06 — jagd-network: Lateral Movement zwischen allen App-Backends möglich

Betroffen: Gesamtes Ökosystem
Datei: {app}/docker-compose.yml

Beschreibung:
Alle sechs Container (3 Backends + 3 Frontends) sind im selben externen Docker-Netzwerk jagd-network. Es gibt keine internen Netzwerk-ACLs. Ein kompromittierter Container kann direkt auf alle anderen Backends zugreifen:

nachsuche-backend → drohnenfuehrer-backend:5000 (direkt erreichbar)
nachsuche-backend → stoeberhunde-backend:5000   (direkt erreichbar)

Empfehlung: Separate interne Netzwerke pro App:

  • nachsuche-internal: nachsuche-backend ↔ nachsuche-mongodb
  • jagd-network (shared): nur Portal-nginx + App-Frontends + App-Backends

H-07 — User-Rollen-JWTs in localStorage (XSS-zugänglich)

Betroffen: Alle drei Apps
Dateien: nachsuche/frontend/src/components/handler/HandlerLogin.js, drohnenfuehrer/frontend/src/services/drohnenfuehrer.js, stoeberhunde/frontend/src/services/stoeberhundefuehrer.js

Beschreibung:
Nach erfolgreichem Login werden die Nutzer-JWTs (Handler, Drohnenführer, Stöberhundeführer) im localStorage gespeichert:

// HandlerLogin.js
localStorage.setItem('handlerToken', result.token);

// drohnenfuehrer.js (interceptor)
const token = localStorage.getItem('drohnenfuehrerToken');

// stoeberhundefuehrer.js (interceptor)
const token = localStorage.getItem('stoeberhundefuehrerToken');

localStorage ist über JavaScript vollständig lesbar und damit bei einer XSS-Lücke (z. B. durch eine Dependency) direkt für Token-Diebstahl angreifbar.

Auswirkung: Jede XSS-Schwachstelle führt zur vollständigen Kompromittierung der Nutzersession.

Empfehlung: Nutzer-Tokens in sessionStorage (kurzlebiger, kein persistierter XSS-Zugriff nach Tab-Schließen) oder besser ebenfalls als httpOnly-Cookie umstellen.


3.3 Mittlere Befunde


M-01 — Passwort-Reset: kein E-Mail-Versand, nur Logging

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/authController.js

Beschreibung:
Die Passwort-Reset-Funktion generiert einen sicheren Token und speichert ihn in der DB. Der Reset-Link wird jedoch nur geloggt, nicht per E-Mail versendet:

logger.info(`Password reset token for ${admin.username}: ${resetUrl}`);
// Kein Mail-Transport implementiert

Auswirkung: In der Produktionsumgebung kann ein Admin sein Passwort nicht zurücksetzen. Ein vergessenes Admin-Passwort erfordert direkten DB-Eingriff.


M-02 — NoSQL-Injection: req.query.type ohne Whitelist-Validierung

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js, getAllUsers und getPublicUsers

Beschreibung:

if (req.query.type) {
  filter.type = req.query.type;   // ← direkt aus dem Query-Parameter
}

Ein Angreifer kann ?type[$ne]=foo oder ?type[$regex]=.* senden. Mongoose schützt durch Schema-Typing (String-Cast) teilweise dagegen, aber ein Typ-Angriff wie ?type[$in][0]=handler&type[$in][1]=drohnenfuehrer ist valides Mongoose und gibt alle Nutzer beider Typen zurück.

Empfehlung: Erlaubte userTypes aus der App-Konfiguration laden und als Whitelist prüfen:

const ALLOWED_TYPES = config.userTypes || [];
if (req.query.type && ALLOWED_TYPES.includes(req.query.type)) {
  filter.type = req.query.type;
}

M-03 — Base64-Foto überschreitet MongoDB-BSON-Limit

Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/userController.js, uploadUserPhoto

Beschreibung:
Fotos werden als Base64-Data-URL direkt im User-Dokument in MongoDB gespeichert. Das BSON-Dokumentlimit beträgt 16 MB. Das Nginx-Limit ist client_max_body_size 20M. Ein 15 MB Foto (nach Base64-Encoding ca. 20 MB) passiert Nginx, schlägt aber beim MongoDB-Speichern mit MongoServerError: Document exceeds maximum size 16777216 fehl ohne sauberes User-Feedback.

Zudem werden bei GET /api/users/export alle Base64-Fotos aller Nutzer inline mitgeliefert, was bei vielen Nutzern zu hunderte-MB-Antworten führen kann.


M-04 — CSP 'unsafe-inline' für script-src und style-src

Betroffen: Alle drei Apps
Datei: {app}/nginx/nginx-frontend.conf

Beschreibung:

add_header Content-Security-Policy "... script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; ...";

'unsafe-inline' in script-src deaktiviert den wesentlichen XSS-Schutz der CSP. Selbst wenn kein dangerouslySetInnerHTML im eigenen Code vorkommt, bleibt eine Dependency-Lücke (z. B. indirektes eval()) ohne CSP-Schutz.

Empfehlung: Nonces oder Hashes statt 'unsafe-inline' verwenden, nachdem Inline-Styles auf CSS-Klassen umgestellt wurden.


M-05 — Geocode-Cache: unbegrenztes In-Memory-Wachstum

Betroffen: Alle drei Apps
Datei: {app}/backend/utils/geocode.js

Beschreibung:

const cache = new Map();  // wächst unbegrenzt

Bei jedem Erstell- und Update-Vorgang wird die Adresse geokodiert und gecacht. Bei einem 500-Nutzer-Import (das dokumentierte Maximum) wachsen alle drei Caches gleichzeitig. Es gibt keine LRU-Eviction oder maximale Cache-Größe.

Zusätzlich läuft der setInterval-Schreibvorgang (alle 60 s) als unkündbarer Timer und verhindert sauberes Graceful-Shutdown.


Betroffen: Alle drei Apps
Datei: {app}/backend/controllers/authController.js

Beschreibung:
sameSite: 'lax' erlaubt das Mitsenden des Cookies bei Top-Level-Navigation (GET-Anfragen von externen Links). Da alle drei App-Backends dasselbe Cookie annehmen (C-01), erhöht 'lax' die CSRF-Angriffsfläche gegenüber 'strict'.


M-07 — nachsuche/nginx/nginx.conf: Dev-Server-Referenzen im Repository

Betroffen: Nachsuche
Datei: nachsuche/nginx/nginx.conf

Beschreibung:
Die Datei nginx/nginx.conf (nicht die korrekte extra8002.conf) enthält Referenzen auf host.docker.internal:5000, host.docker.internal:8080 sowie explizite Dev-Ports (5001, 5002, 8081, 8082). Falls diese Datei versehentlich als aktive Konfiguration deployt wird (statt extra8002.conf), werden Backend-Requests direkt an den Host-Daemon weitergeleitet und umgehen die Container-Isolation.


M-08 — User-Rollen-Login ohne striktes Rate-Limiting

Betroffen: Alle drei Apps
Datei: {app}/backend/routes/handlerRoutes.js, drohnenfuehrerRoutes.js, stoeberhundefuehrerRoutes.js

Beschreibung:

router.post('/login', handlerLogin);  // ← kein authLimiter, nur globaler apiLimiter (100/15min)

Der Admin-Login ist mit authLimiter (5 Versuche/15 min) geschützt. User-Logins sind nur durch den generellen apiLimiter (100 Anfragen/15 min) abgedeckt. Ein Angreifer kann bis zu 100 Passwort-Versuche pro 15 Minuten gegen jeden Nutzer-Account durchführen.


3.4 Niedrige Befunde


L-01 — bcrypt Work-Factor-Inkonsistenz: Admin=10, User=12

Betroffen: Alle drei Apps
Dateien: {app}/backend/models/Admin.js (pre-save hook: genSalt(10)) vs. {app}/backend/controllers/{rolle}Controller.js (bcrypt.hash(pwd, 12))

Admin-Passwörter werden mit einem niedrigeren Work-Factor gehasht als Nutzer-Passwörter. Beide sollten auf mindestens 12 vereinheitlicht werden.


L-02 — Admin-Mindestpasswortlänge: 6 Zeichen (zu kurz)

Betroffen: Alle drei Apps
Datei: {app}/backend/models/Admin.js

passwordHash: { type: String, required: true, minlength: 6 }

Für Admin-Konten ist eine Mindestlänge von 12 Zeichen empfohlen (NIST SP 800-63B).


L-03 — Frontend-Passwortvalidierung: 6 Zeichen, Backend erfordert 8

Betroffen: Alle drei Apps
Dateien: {app}/frontend/src/components/{rolle}/{rolle}Login.js

Das Passwort-Setzen-Formular prüft newPassword.length < 6, das Backend fordert jedoch min. 8 Zeichen. Formular und Backend sind inkonsistent (sobald H-04 behoben ist, führt dies zu verwirrenden Fehlermeldungen).


L-04 — Logger service-Name falsch in allen drei Apps

Betroffen: Alle drei Apps
Datei: {app}/backend/utils/logger.js

defaultMeta: { service: 'tracking-leaders-api' }  // identisch in allen 3 Apps

Bei zentralem Log-Aggregation (ELK, Loki, Datadog) sind die Logs der drei Apps nicht unterscheidbar.

Fix: 'nachsuche-api', 'drohnenfuehrer-api', 'stoeberhunde-api'.


L-05 — child_process.exec('node seed.js') beim Startup

Betroffen: Alle drei Apps
Datei: {app}/backend/server.js

exec('node seed.js', { cwd: __dirname }, callback)

exec() verwendet eine Shell und ist PATH-abhängig. Der Exit-Code des Seed-Prozesses wird nicht geprüft. Bei Seed-Fehlern startet der Server ohne initiale Daten, ohne dass eine Warnung ausgegeben wird.

Empfehlung: require('./seed')() als Modul aufrufen kein Sub-Prozess, korrekte Fehlerweiterleitung.


L-06 — Geocode-Cache-Datei im Container (kein Volume-Mount)

Betroffen: Alle drei Apps
Datei: {app}/backend/utils/geocode.js

const CACHE_FILE = path.join(process.cwd(), 'geocode-cache.json');

Die Geocode-Cache-Datei wird im Container-Dateisystem geschrieben. Bei docker compose down / docker compose up ist der Cache verloren, und alle Adressen werden erneut über die Nominatim-API abgefragt (Rate-Limit: 1 Request/Sekunde).


L-07 — Fehlende Offline-Unterstützung für Feld-Apps

Betroffen: Nachsuche, Drohnenfuehrer, Stoeberhunde

Die Apps sind für den Feldeinsatz (Wald, schlechte Mobilfunkabdeckung) konzipiert. Keiner der drei Apps hat einen Service Worker. Bricht die Netzwerkverbindung ab, sind alle Karten und Benutzerdaten sofort unzugänglich. Nur das Portal hat einen minimalen SW mit Offline-Fallback-Seite.


4. System-spezifische Befunde

4.1 Portal

# Befund Priorität
C-01 (teilweise) Gemeinsamer JWT-Secret-Namespace über alle Apps KRITISCH
portal/extra8002.conf korrekt mit Container-DNS-Namen konfiguriert ✓ OK
Statisches Proxy-Routing, kein SSO, kein API-Gateway Architekturentscheidung
Service Worker vorhanden mit Offline-Fallback ✓ OK

4.2 Nachsuche

# Befund Priorität
C-01 Cross-App-Admin-Auth KRITISCH
C-02 overwrite undeclared in importUsers KRITISCH
H-01 / H-02 passwordHash in API-Antworten HOCH
H-03 Mass Assignment in createUser/updateUser HOCH
H-04 Set-Password-Flow defekt (kein inviteToken im Frontend) HOCH
H-07 Handler-JWT in localStorage HOCH
M-07 Dev-nginx.conf mit host.docker.internal im Repo MITTEL
Cookie secure: secureCookie korrekt bedingt ✓ OK

4.3 Drohnenfuehrer

# Befund Priorität
C-01 Cross-App-Admin-Auth KRITISCH
C-02 overwrite undeclared in importUsers KRITISCH
H-01 / H-02 passwordHash in API-Antworten HOCH
H-03 Mass Assignment in createUser/updateUser HOCH
H-04 Set-Password-Flow defekt HOCH
H-07 Drohnenführer-JWT in localStorage HOCH
Cookie secure: secureCookie korrekt bedingt ✓ OK

4.4 Stoeberhunde

# Befund Priorität
C-01 Cross-App-Admin-Auth KRITISCH
C-02 overwrite undeclared in importUsers KRITISCH
H-01 / H-02 passwordHash in API-Antworten HOCH
H-03 Mass Assignment in createUser/updateUser HOCH
H-04 Set-Password-Flow defekt HOCH
H-05 Cookie secure: false hardcoded HOCH
H-07 Stöberhundeführer-JWT in localStorage HOCH

5. Positive Befunde

Das Ökosystem zeigt in mehreren Bereichen solide Sicherheitspraktiken:

Bereich Befund
Auth Admin-JWT als httpOnly-Cookie nicht per JS zugänglich
Auth authLimiter: 5 Admin-Login-Versuche / 15 Minuten
Auth inviteLimiter: 10 Set-Password-Versuche / 15 Minuten
Auth timingSafeEqual für Invite-Token-Vergleich (Timing-Attack-Schutz)
Auth User-Enumeration in forgotPassword verhindert (immer gleiche Antwort)
Auth JWT_SECRET: Default-Wert in Production erkannt → Startup-Crash
API express-validator für Login-Endpunkte (validateLogin)
API Paginierung mit hartem Maximum (100 Items/Seite)
API Benutzer-Selbstverwaltung (/me) mit explizit aufgebauter Update-Whitelist
Daten Soft-Delete-Pattern korrekt implementiert (gelöschte User nie in Queries)
Daten auditLogger entfernt passwordHash aus Before/After-Snapshots
Daten GPS-Koordinaten doppelt validiert: express-validator + Mongoose min/max
Infra Backend-Ports nur auf 127.0.0.1 gebunden
Infra MongoDB nicht extern exponiert
Infra Graceful SIGTERM-Shutdown implementiert
Frontend Axios-Retry mit exponentiellem Backoff (2 Versuche, max. 5 s)
Nginx X-Frame-Options: DENY, X-Content-Type-Options: nosniff gesetzt
Nginx HSTS-Header in App-nginx.conf vorhanden

6. Priorisierte Handlungsempfehlungen

Sofort (vor nächstem Deployment)

1. C-01 App-isolierte JWT-Secrets einführen

# nachsuche/docker-compose.yml
environment:
  JWT_SECRET: ${NACHSUCHE_JWT_SECRET}
  
# drohnenfuehrer/docker-compose.yml
environment:
  JWT_SECRET: ${DROHNENFUEHRER_JWT_SECRET}

Und App-Claim im Token + Prüfung in authenticateToken ergänzen.

2. C-02 overwrite-Deklaration in importUsers ergänzen
In allen drei userController.js nach Zeile 466 einfügen:

const overwrite = req.query.overwrite === 'true';

3. H-04 Set-Password-Flow reparieren
Im Formular ein inviteToken-Eingabefeld ergänzen; Admin zeigt den generierten Token im UI (oder der Link enthält ihn als Query-Parameter).

4. H-05 Stoeberhunde secure: false korrigieren

const secureCookie = process.env.NODE_ENV === 'production';
secure: secureCookie,

Kurzfristig (innerhalb 2 Wochen)

5. H-01/H-02 passwordHash aus Admin-API-Antworten entfernen

User.find(filter).select('-__v -passwordHash')
User.findById(req.params.id).select('-passwordHash -__v')

Alternativ im Schema: passwordHash: { ..., select: false }.

6. H-03 Mass Assignment durch Feldwhitelist ersetzen
Explizite Feldliste für createUser und updateUser analog zu bulkUpdateUsers.

7. H-07 JWTs aus localStorage migrieren
sessionStorage als Minimalmaßnahme; httpOnly-Cookie als optimale Lösung.

8. H-06 Netzwerk-Segmentierung einführen
Pro App ein internes Netzwerk für Backend ↔ MongoDB-Kommunikation; jagd-network nur für Portal-seitige Kommunikation.

9. M-08 authLimiter auf User-Rollen-Logins anwenden

router.post('/login', authLimiter, handlerLogin);

Mittelfristig (innerhalb 30 Tage)

10. M-01 E-Mail-Transport implementieren
Nodemailer + SMTP-Konfiguration via Umgebungsvariablen für Password-Reset und Invite-Token-Versand.

11. M-02 req.query.type Whitelist-Validierung
Erlaubte User-Typen aus Konfiguration laden und gegen Query-Parameter prüfen.

12. M-03 Base64-Foto-Größe serverseitig begrenzen

const MAX_PHOTO_SIZE = 2 * 1024 * 1024; // 2 MB Base64
if (photo.length > MAX_PHOTO_SIZE) return res.status(413).json(...);

13. M-04 CSP 'unsafe-inline' eliminieren
Nonce-basierte CSP nach CRA/Vite-Build-Integration einführen.

14. M-05 Geocode-Cache LRU-Limit einführen

// Max 1000 Einträge, älteste werden verdrängt
if (cache.size >= 1000) {
  const firstKey = cache.keys().next().value;
  cache.delete(firstKey);
}

Und setInterval.unref() für sauberes Graceful-Shutdown.

15. M-06 sameSite: 'strict' für Admin-Cookie

16. M-07 Dev-nginx.conf umbenennen oder entfernen
nachsuche/nginx/nginx.confnginx.conf.dev.bak oder in .gitignore.

Langfristig (innerhalb 90 Tage)

17. L-01 bcrypt Work-Factor vereinheitlichen auf 12 für Admin und User.

18. L-02/L-03 Admin-Mindestpasswortlänge auf 12 Zeichen anheben
Formular- und Backend-Validierung synchronisieren.

19. L-04 Logger-Service-Namen app-spezifisch setzen.

20. L-05 exec('node seed.js') durch require('./seed')() ersetzen.

21. L-06 Geocode-Cache-Datei via Volume-Mount persistieren:

volumes:
  - geocode-cache:/app/geocode-cache.json

22. L-07 Service Worker für Feld-Apps implementieren
Leaflet-Kacheln und letzte Benutzerliste für Offline-Betrieb cachen (Cache-First-Strategie für statische Assets, Network-First für API-Calls mit Fallback).


7. Technischer Anhang

7.1 Dependency-Versionen (Stand Audit)

Paket Version Anmerkung
express 4.18.2 aktuell
mongoose 7.5.0 aktuell
jsonwebtoken 9.0.2 aktuell
helmet 8.0.0 aktuell
express-rate-limit 8.2.1 aktuell
bcryptjs 2.4.3 aktuell
express-validator 7.3.1 aktuell
winston 3.19.0 aktuell
axios 1.13.5 aktuell
react 18.2.0 aktuell
leaflet 1.9.4 aktuell
node (Docker base) 22-alpine aktuell
nginx (Docker base) alpine aktuell
mongodb 7.0 aktuell

package-lock.json enthält Hinweise auf veraltete glob-Versionen als transitive Abhängigkeiten (via dev-dependencies, kein Produktionsrisiko).

7.2 Befund-Index (Schnellreferenz)

ID Titel Priorität Betroffen
C-01 Cross-App-Admin-Auth durch gemeinsamen JWT_SECRET KRITISCH Alle
C-02 overwrite undeclared → ReferenceError in importUsers KRITISCH Alle
H-01 passwordHash in getAllUsers exponiert HOCH Alle
H-02 passwordHash in getUserById exponiert HOCH Alle
H-03 Mass Assignment in createUser/updateUser HOCH Alle
H-04 Set-Password-Flow defekt (inviteToken fehlt im Frontend) HOCH Alle
H-05 secure: false für Admin-Cookie hardcoded HOCH Stoeberhunde
H-06 Lateral Movement über gemeinsames jagd-network HOCH Alle
H-07 Nutzer-JWTs in localStorage (XSS-zugänglich) HOCH Alle
M-01 Passwort-Reset: kein E-Mail-Versand MITTEL Alle
M-02 NoSQL-Filter: req.query.type ohne Whitelist MITTEL Alle
M-03 Base64-Foto überschreitet BSON-16MB-Limit MITTEL Alle
M-04 CSP unsafe-inline in script-src und style-src MITTEL Alle
M-05 Geocode-Cache unbegrenzt im Speicher MITTEL Alle
M-06 sameSite: 'lax' statt 'strict' für Admin-Cookie MITTEL Alle
M-07 Dev-nginx.conf mit host.docker.internal im Repo MITTEL Nachsuche
M-08 Nutzer-Rollen-Login ohne authLimiter MITTEL Alle
L-01 bcrypt Work-Factor: Admin=10, User=12 NIEDRIG Alle
L-02 Admin-Mindestpasswortlänge 6 Zeichen NIEDRIG Alle
L-03 Frontend-Passwortlänge (6) != Backend-Minimum (8) NIEDRIG Alle
L-04 Logger-Service-Name: 'tracking-leaders-api' in allen Apps NIEDRIG Alle
L-05 exec('node seed.js') via child_process NIEDRIG Alle
L-06 Geocode-Cache-Datei im Container (kein Volume) NIEDRIG Alle
L-07 Fehlende Offline-Unterstützung für Feld-Apps NIEDRIG Alle

Ende des Audit-Berichts 3. Mai 2026