Skip to content

How-to: Persönliche Geheimnis-Tresor-API

Demonstriert benutzerspezifischen Key-Value-Speicher mit HMAC-Integrität, IDOR-Prävention und nur-Admin-Metadatenzugriff. Feldversuch: FT195 (../NENE2-FT/vaultlog/). Enthält VULN-A bis L Sicherheitsprüfung.


Muster-Zusammenfassung

ProblemAnsatz
BenutzerisolationWHERE user_id = :uid bei jeder Abfrage — IDOR unmöglich
Admin sieht niemals WerteAdmin-Endpunkte geben nur user_id + key zurück
HMAC-Integrität`HMAC-SHA256(userId
Key-Validierungpreg_match('/\A[a-z0-9_-]{1,64}\z/', $key) — sicher, kein ReDoS-Risiko
Benutzer-ID-Validierungctype_digit() + Längenguard + > 0-Prüfung
Admin-Keyhash_equals() zeitkonstant, fail-closed bei leerem Key
UpsertUNIQUE(user_id, key_name) → erste Speicherung (201) oder Aktualisierung (200)

Routen

MethodePfadAuthBeschreibung
POST/vaultX-User-IdGeheimnis speichern oder aktualisieren
GET/vaultX-User-IdGeheimnis-Keys des Benutzers auflisten (keine Werte)
GET/vault/{key}X-User-IdGeheimnis-Wert des Benutzers abrufen
DELETE/vault/{key}X-User-IdGeheimnis des Benutzers löschen
GET/admin/vaultX-Admin-KeyAlle Benutzer + Keys auflisten (keine Werte)
GET/admin/vault/{userId}X-Admin-KeyKeys eines bestimmten Benutzers auflisten

Datenbankschema

sql
CREATE TABLE vault_entries (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL,
    key_name   TEXT    NOT NULL,
    value      TEXT    NOT NULL,
    hmac       TEXT    NOT NULL,   -- HMAC-SHA256-Integritäts-Tag
    created_at TEXT    NOT NULL,
    updated_at TEXT    NOT NULL,
    UNIQUE (user_id, key_name)
);

Der UNIQUE(user_id, key_name)-Constraint erzwingt einen Eintrag pro (Benutzer, Key)-Paar.


HMAC-Integrität

php
private function computeHmac(int $userId, string $key, string $value): string
{
    return hash_hmac('sha256', "{$userId}|{$key}|{$value}", $this->hmacSecret);
}

Bei GET verifiziert der Handler den gespeicherten HMAC:

php
if (!$this->repo->verifyIntegrity($entry)) {
    return $this->problem(500, 'integrity-error', 'Geheimnis-Integritätsprüfung fehlgeschlagen.');
}

Das erkennt direkte DB-Manipulation (z.B. ein kompromittierter DBA, der Werte ohne die API ändert).


IDOR-Prävention

Jede Abfrage enthält user_id = :uid:

sql
SELECT * FROM vault_entries WHERE user_id = :uid AND key_name = :key

Benutzer 200, der Key private-key von Benutzer 100 abfragt, erhält 404 — identisch mit "nicht gefunden", verhindert die Enumeration welche Keys für andere Benutzer existieren.

Admin-Endpunkte geben niemals value zurück:

php
// Benutzer sieht seinen eigenen Wert
public function toUserArray(): array
{
    return ['key' => ..., 'value' => $this->value, ...];
}

// Admin sieht nur Metadaten — keinen Wert
public function toAdminArray(): array
{
    return ['user_id' => ..., 'key' => ..., ...];
}

Key-Validierung

php
private const string KEY_PATTERN = '/\A[a-z0-9_-]{1,64}\z/';

\A- und \z-Anker verhindern Teilübereinstimmungen. Die Zeichenklasse ist minimal: Kleinbuchstaben alphanumerisch, Bindestrich, Unterstrich. Länge ist begrenzt {1,64} — keine Backtracking-Amplifikation.

Das lehnt ab:

  • Großbuchstaben (UPPER_CASE)
  • Leerzeichen oder Sonderzeichen
  • Path-Traversal-Fragmente (../etc/passwd)
  • SQL-injizierbare Strings (' OR '1'='1)
  • Leerer String oder Strings > 64 Zeichen

Benutzer-ID-Validierung

php
private function resolveUserId(ServerRequestInterface $request): ?int
{
    $raw = $request->getHeaderLine('X-User-Id');
    if ($raw === '' || !ctype_digit($raw) || strlen($raw) > 18) return null;
    $id = (int) $raw;
    return $id > 0 ? $id : null;
}
  • ctype_digit() lehnt negative Zahlen ab (das --Zeichen ist keine Ziffer)
  • strlen > 18 verhindert Integer-Überlauf (PHP_INT_MAX hat 19 Ziffern)
  • > 0 lehnt "0" als ungültige Benutzer-ID ab

Upsert-Muster

php
public function store(int $userId, string $key, string $value): string
{
    $existing = $this->findEntry($userId, $key);
    if ($existing !== null) {
        // UPDATE ...
        return 'updated';  // → 200
    }
    // INSERT ...
    return 'stored';  // → 201
}

Gibt 'stored' (201) beim ersten Schreiben zurück, 'updated' (200) beim Überschreiben. Der Handler ordnet diese den HTTP-Statuscodes zu.


VULN-A bis L Ergebnisse

PrüfungTestErgebnis
VULN-ASQL-Injection in Key-Param / BodyBESTANDEN — Key-Validierung lehnt vor Abfrage ab
VULN-BIDOR: Benutzer liest/löscht Key eines anderenBESTANDEN — 404 bei benutzerübergreifendem Zugriff
VULN-CListe gibt nur eigene Einträge zurückBESTANDEN — WHERE user_id begrenzt
VULN-DAdmin-Key Brute-Force / BypassBESTANDEN — hash_equals + fail-closed
VULN-EXSS im WertBESTANDEN — als-ist gespeichert, JSON-Antwort kein HTML
VULN-FKey-Upsert-IdempotenzBESTANDEN — letzter Schreibvorgang gewinnt, keine Duplikate
VULN-GPath-Traversal im KeyBESTANDEN — Muster lehnt .. und Schrägstriche ab
VULN-HNegative / null Benutzer-IDBESTANDEN — ctype_digit + > 0 Guard
VULN-ISehr große Benutzer-ID (Überlauf)BESTANDEN — strlen > 18 Guard
VULN-JNull-Byte im PfadBESTANDEN — Router / Muster lehnen ab
VULN-KÜberlanger Key im BodyBESTANDEN — 422 Validierung
VULN-LLeeres HMAC-Secret (kein Absturz)BESTANDEN — deterministischer HMAC mit leerem Key, kein Absturz

Test-Hinweise

  • AppFactory::create(?PDO, ?string adminKey, ?string hmacSecret) — alle injizierbar für Unit-Tests.
  • withParsedBody($body) ist in Test-Helfern erforderlich (Nyholm PSR-7 parst JSON nicht automatisch).
  • IDOR-Tests: als Benutzer 100 speichern, Zugriff als Benutzer 200 versuchen → muss 404 erhalten.
  • Admin-Tests: überprüfen, dass value-Key in jedem Antwort-Array fehlt.

Veröffentlicht unter der MIT-Lizenz.