Dienstkonto-Flag: Gate in beide Richtungen, richtige Rechte-Domaene (R184)
R184-01: Setzen war gegatet, Entfernen nicht - und das Entfernen ist der gefaehrlichere Weg. Der Heartbeat-Wachhund fragt isServiceAccount:true ab, haengt also am Live-Kennzeichen; die Hypothese des Pentesters war richtig, Un-Flaggen kappt die Wache. Genau das stilllegen-und-auf-Stille-setzen, gegen das der Tripwire gebaut wurde. Fix: Gate in beide Richtungen mit eigenem Wortlaut beim Entfernen. Wichtiger noch: Die Aenderung geht jetzt zusaetzlich in den Alarmkanal (PERMISSION_CHANGED/CRITICAL), nicht nur ins Audit-Log - eine CRITICAL-Zeile muss jemand lesen, das war die R183-02-Klasse. R184-02: Das Kennzeichen hing an users:update, obwohl es ein Audit-Governance-Eingriff ist - Geschwister von retention-shorten, seal-backlog, rehash und cleanup, die alle audit:admin verlangen. Heute deckungsgleich, aber jede kuenftige Rolle mit Benutzer-bearbeiten haette still Login-Alarme-herunterstufen geerbt. Fix: zusaetzliche Pruefung auf audit:admin. Verifiziert: Entfernen ohne Bestaetigung jetzt 400 statt 200, ohne audit:admin 403, zwei CRITICAL-Meldungen im Alarmkanal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import prisma from '../lib/prisma.js';
|
|||||||
import * as userService from '../services/user.service.js';
|
import * as userService from '../services/user.service.js';
|
||||||
import { logChange } from '../services/audit.service.js';
|
import { logChange } from '../services/audit.service.js';
|
||||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||||
|
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
|
||||||
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
|
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
|
||||||
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';
|
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';
|
||||||
|
|
||||||
@@ -175,6 +176,25 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
|||||||
(data as any).isServiceAccount !== before?.isServiceAccount;
|
(data as any).isServiceAccount !== before?.isServiceAccount;
|
||||||
const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true;
|
const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true;
|
||||||
|
|
||||||
|
// R184-02: Das Kennzeichen ist ein Audit-Governance-Eingriff, kein
|
||||||
|
// gewoehnliches Benutzerfeld – es ist Geschwister von retention-shorten,
|
||||||
|
// seal-backlog, rehash und cleanup, die alle `audit:admin` verlangen. Es
|
||||||
|
// haengt aber an der Route mit `users:update`. Heute deckungsgleich, weil
|
||||||
|
// nur die Administratorrolle beides hat – aber jede kuenftige Rolle mit
|
||||||
|
// „Benutzer bearbeiten“ wuerde still „Login-Alarme herunterstufen“ erben.
|
||||||
|
// Deshalb hier zusaetzlich auf die Domaene pruefen, in der der Schaden
|
||||||
|
// liegt.
|
||||||
|
if (setztDienstkonto && !(req.user?.permissions || []).includes('audit:admin')) {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
'Das Dienstkonto-Kennzeichen ändert, wie Anmeldungen im Audit-Log bewertet werden. ' +
|
||||||
|
'Dafür genügt „Benutzer bearbeiten“ nicht – es erfordert die Berechtigung ' +
|
||||||
|
'audit:admin, wie das Verkürzen von Aufbewahrungsfristen auch.',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (setztDienstkonto && req.user?.userId === userId) {
|
if (setztDienstkonto && req.user?.userId === userId) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -187,14 +207,23 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aktiviertDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
|
// Gate in BEIDE Richtungen (Pentest R184-01). Das Entfernen war ungegatet –
|
||||||
|
// und es ist der gefaehrlichere Weg: Ein un-geflaggtes Konto faellt aus der
|
||||||
|
// Heartbeat-Wache, weil diese am Live-Kennzeichen haengt. Genau das
|
||||||
|
// „stilllegen und auf Stille setzen“, gegen das der Wachhund gebaut wurde.
|
||||||
|
if (setztDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: aktiviertDienstkonto
|
||||||
`Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im Audit-Log ` +
|
? `Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im ` +
|
||||||
'als Routine geführt statt als kritisches Ereignis. Das ist für planmäßig arbeitende ' +
|
'Audit-Log als Routine geführt statt als kritisches Ereignis. Das ist für ' +
|
||||||
'Dienste gedacht (etwa das Gegenbuch) – für ein Konto, das ein Mensch benutzt, wäre es ' +
|
'planmäßig arbeitende Dienste gedacht (etwa das Gegenbuch) – für ein Konto, das ' +
|
||||||
'eine Tarnung. Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
|
'ein Mensch benutzt, wäre es eine Tarnung. ' +
|
||||||
|
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.'
|
||||||
|
: `Damit fällt ${before?.email ?? 'dieses Konto'} aus der Überwachung heraus: Es wird ` +
|
||||||
|
'dann nicht mehr gemeldet, wenn es sich nicht mehr anmeldet. Bei einem laufenden ' +
|
||||||
|
'Dienst wie dem Gegenbuch hieße das, sein Ausfall bliebe unbemerkt. ' +
|
||||||
|
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -239,6 +268,29 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
|||||||
sensitivity: setztDienstkonto ? 'CRITICAL' : undefined,
|
sensitivity: setztDienstkonto ? 'CRITICAL' : undefined,
|
||||||
details: Object.keys(changes).length > 0 ? changes : undefined,
|
details: Object.keys(changes).length > 0 ? changes : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Zusaetzlich in den Alarmkanal (Pentest R184-01). Eine CRITICAL-Zeile
|
||||||
|
// im Audit-Log muss jemand LESEN – das ist die R183-02-Klasse. Das
|
||||||
|
// Entfernen des Kennzeichens nimmt das Konto aus der Heartbeat-Wache;
|
||||||
|
// ab dann faellt sein Ausbleiben nicht mehr auf. Diese Aenderung
|
||||||
|
// gehoert deshalb dorthin, wo automatisch reagiert wird.
|
||||||
|
if (setztDienstkonto) {
|
||||||
|
const ctx = contextFromRequest(req);
|
||||||
|
emitSecurityEvent({
|
||||||
|
type: 'PERMISSION_CHANGED',
|
||||||
|
severity: 'CRITICAL',
|
||||||
|
message: aktiviertDienstkonto
|
||||||
|
? `Konto ${user.email} als Dienstkonto markiert – seine Anmeldungen gelten ab ` +
|
||||||
|
'jetzt als Routine statt als kritisches Ereignis.'
|
||||||
|
: `Konto ${user.email} ist KEIN Dienstkonto mehr – es fällt damit aus der ` +
|
||||||
|
'Überwachung heraus: Ein Ausbleiben seiner Anmeldungen wird nicht mehr gemeldet.',
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userId: req.user?.userId,
|
||||||
|
userEmail: req.user?.email,
|
||||||
|
endpoint: ctx.endpoint,
|
||||||
|
details: { betroffenesKonto: user.email, aktiviert: aktiviertDienstkonto },
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await logChange({
|
await logChange({
|
||||||
req, action: 'UPDATE', resourceType: 'User',
|
req, action: 'UPDATE', resourceType: 'User',
|
||||||
|
|||||||
@@ -97,6 +97,32 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🔓 Dienstkonto-Flag: Gate in beide Richtungen, richtige Rechte-Domaene (Pentest R184-01/-02)** (2026-08-24)
|
||||||
|
- **R184-01** – Setzen war gegatet, **Entfernen nicht**. Und das Entfernen ist
|
||||||
|
der gefaehrlichere Weg: Der Heartbeat-Wachhund fragt `isServiceAccount:
|
||||||
|
true` ab, haengt also am **Live-Kennzeichen**. Seine Hypothese war damit
|
||||||
|
richtig – Un-Flaggen kappt die Wache und macht genau das moeglich, wogegen
|
||||||
|
der Tripwire gebaut wurde: stilllegen und auf Stille setzen.
|
||||||
|
Fix: Gate in beide Richtungen, mit unterschiedlichem Wortlaut (beim
|
||||||
|
Entfernen: „faellt aus der Ueberwachung heraus“).
|
||||||
|
- **Wichtiger noch:** Die Aenderung geht jetzt zusaetzlich in den
|
||||||
|
**Alarmkanal** (`PERMISSION_CHANGED`/CRITICAL), nicht nur ins Audit-Log.
|
||||||
|
Eine CRITICAL-Zeile muss jemand LESEN – das war die R183-02-Klasse. Jetzt
|
||||||
|
reagiert die Ueberwachung automatisch.
|
||||||
|
- **R184-02** – Das Kennzeichen hing an `users:update`, einer
|
||||||
|
Anwendungs-Berechtigung, obwohl es ein Audit-Governance-Eingriff ist –
|
||||||
|
Geschwister von retention-shorten, seal-backlog, rehash und cleanup, die
|
||||||
|
alle `audit:admin` verlangen. Heute deckungsgleich, aber jede kuenftige
|
||||||
|
Rolle mit „Benutzer bearbeiten“ haette still „Login-Alarme herunterstufen“
|
||||||
|
geerbt. Fix: zusaetzliche Pruefung auf `audit:admin` im Controller.
|
||||||
|
- Verifiziert: Setzen mit `audit:admin` 200 · Entfernen ohne Bestaetigung
|
||||||
|
**400** (vorher 200) · mit Bestaetigung 200 · ohne `audit:admin` **403** ·
|
||||||
|
zwei CRITICAL-Meldungen im Alarmkanal mit sprechendem Text. Frontend sendet
|
||||||
|
die Bestaetigung in beide Richtungen. `npm run build` und Backend-`tsc`
|
||||||
|
gruen.
|
||||||
|
- Seine Non-Findings uebernommen: Selbst-Block nicht ueber Methode, Pfad oder
|
||||||
|
ID-Aliasing umgehbar; Create streift das Feld ab; Confirm-Gate strikt.
|
||||||
|
|
||||||
- [x] **🔒 Dienstkonto-Kennzeichen gegatet, laut protokolliert, kein Selbstbedienen (Pentest R184)** (2026-08-24)
|
- [x] **🔒 Dienstkonto-Kennzeichen gegatet, laut protokolliert, kein Selbstbedienen (Pentest R184)** (2026-08-24)
|
||||||
- Der Pentester hat sofort erkannt, was das Scharfschalten des Feldes
|
- Der Pentester hat sofort erkannt, was das Scharfschalten des Feldes
|
||||||
bedeutet: Ein Attribut, das die **Alarmstufe senkt**, war ueber den
|
bedeutet: Ein Attribut, das die **Alarmstufe senkt**, war ueber den
|
||||||
|
|||||||
@@ -331,7 +331,10 @@ function UserModal({
|
|||||||
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
||||||
// Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken
|
// Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken
|
||||||
// im Formular IST diese Bestätigung.
|
// im Formular IST diese Bestätigung.
|
||||||
...(formData.isServiceAccount ? { confirm: 'SERVICE_ACCOUNT' } : {}),
|
// Das Kennzeichen senkt bzw. hebt die Bewertung der Anmeldungen dieses
|
||||||
|
// Kontos. Der Server verlangt in BEIDE Richtungen eine ausdrückliche
|
||||||
|
// Bestätigung; der Haken im Formular ist diese Bestätigung.
|
||||||
|
confirm: 'SERVICE_ACCOUNT',
|
||||||
whatsappNumber: formData.whatsappNumber || undefined,
|
whatsappNumber: formData.whatsappNumber || undefined,
|
||||||
telegramUsername: formData.telegramUsername || undefined,
|
telegramUsername: formData.telegramUsername || undefined,
|
||||||
signalNumber: formData.signalNumber || undefined,
|
signalNumber: formData.signalNumber || undefined,
|
||||||
@@ -371,7 +374,10 @@ function UserModal({
|
|||||||
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
||||||
// Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken
|
// Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken
|
||||||
// im Formular IST diese Bestätigung.
|
// im Formular IST diese Bestätigung.
|
||||||
...(formData.isServiceAccount ? { confirm: 'SERVICE_ACCOUNT' } : {}),
|
// Das Kennzeichen senkt bzw. hebt die Bewertung der Anmeldungen dieses
|
||||||
|
// Kontos. Der Server verlangt in BEIDE Richtungen eine ausdrückliche
|
||||||
|
// Bestätigung; der Haken im Formular ist diese Bestätigung.
|
||||||
|
confirm: 'SERVICE_ACCOUNT',
|
||||||
whatsappNumber: formData.whatsappNumber || undefined,
|
whatsappNumber: formData.whatsappNumber || undefined,
|
||||||
telegramUsername: formData.telegramUsername || undefined,
|
telegramUsername: formData.telegramUsername || undefined,
|
||||||
signalNumber: formData.signalNumber || undefined,
|
signalNumber: formData.signalNumber || undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user