Timing-Attacken und Constant-Time-Vergleiche
AI generated
OWASP
0x00
OWASP Kryptografie · Timing-Seitenkanäle
Timing-Attacken und Constant-Time-Vergleiche
Wenn die Ausführungszeit selbst ein Geheimnis verrät

Unterschiedliche Ausführungszeiten bei String-Vergleichen können Angreifern erlauben, geheime Werte wie API-Keys oder HMAC-Signaturen Byte für Byte zu erraten. Wir erklären das Prinzip, zeigen hash_equals() als Lösung in PHP und welche Vergleiche überhaupt timing-sensitiv sind.

12 Min. Lesezeit Timing Attacks hash_equals()

1. Was sind Timing-Attacken?

Eine Timing-Attacke nutzt die Tatsache aus, dass die Ausführungszeit eines Programms von den verarbeiteten Werten abhängen kann, obwohl das Ergebnis der Operation nach außen hin identisch aussieht. Bei einem naiven String-Vergleich, wie ihn der Operator === in PHP intern durchführt, wird byteweise verglichen und der Vergleich beim ersten abweichenden Byte sofort abgebrochen. Diese Optimierung ist für normale Anwendungsfälle vollkommen sinnvoll, wird aber zum Problem, sobald einer der verglichenen Werte geheim sein soll.

Ein Angreifer, der viele Anfragen mit unterschiedlichen Rateversuchen stellt und dabei die Antwortzeit misst, kann daraus Rückschlüsse ziehen, wie viele Bytes am Anfang bereits korrekt waren. Stimmen mehr Bytes am Anfang überein, dauert der Vergleich minimal länger, weil er erst später abbricht. Ueber ausreichend viele Messungen lässt sich ein geheimer Wert so Byte für Byte rekonstruieren.

2. Wo das Problem in der Praxis auftritt

Timing-sensitiv sind vor allem Vergleiche, bei denen ein geheimer, serverseitig bekannter Wert mit einem vom Client übermittelten Wert verglichen wird: API-Keys in Header-Feldern, HMAC-Signaturen bei Webhook-Validierungen, Passwort-Reset-Token oder CSRF-Token. Ueberall dort, wo ein einfacher ===-Vergleich zwischen einem geheimen und einem angreiferkontrollierten Wert steht, besteht theoretisch ein Timing-Risiko.

Die verwundbare Methode im folgenden Beispiel nutzt den Standardvergleichsoperator, dessen Ausführungszeit theoretisch mit der Anzahl übereinstimmender Anfangsbytes korreliert. Die abgesicherte Methode verwendet stattdessen hash_equals, das intern immer alle Bytes beider Werte vergleicht, unabhängig davon, wie früh eine Abweichung auftritt, und damit eine konstante Laufzeit unabhängig vom Inhalt garantiert.


<?php

declare(strict_types=1);

namespace App\Security;

final class WebhookSignatureValidator
{
    public function __construct(
        private readonly string $webhookSecret,
    ) {
    }

    // Verwundbar: Byteweiser Vergleich bricht beim ersten Unterschied ab
    public function isValidVulnerable(string $payload, string $providedSignature): bool
    {
        $expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret);

        return $expectedSignature === $providedSignature;
    }

    // Abgesichert: Konstante Vergleichszeit unabhängig vom Inhalt
    public function isValidSecured(string $payload, string $providedSignature): bool
    {
        $expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret);

        return hash_equals($expectedSignature, $providedSignature);
    }
}

3. Warum hash_equals() das Problem löst

hash_equals wurde speziell für sicherheitskritische Vergleiche entwickelt und ist seit PHP 5.6 Teil der Standardbibliothek. Die Funktion vergleicht zwei Strings so, dass die benötigte Zeit ausschließlich von der Länge der Strings abhängt, nicht aber davon, an welcher Stelle ein Unterschied auftritt. Intern werden alle Bytes verglichen und die Ergebnisse über eine bitweise Verknüpfung zusammengeführt, statt beim ersten Unterschied abzubrechen.

Wichtig ist, dass hash_equals ausschließlich für den Vergleich von zwei bereits bekannten, feststehenden Werten gedacht ist, etwa einer erwarteten und einer erhaltenen Signatur. Für allgemeine String-Vergleiche ohne sicherheitskritischen Kontext bleibt der normale ===-Operator weiterhin die richtige und performantere Wahl.

4. Welche Vergleiche überhaupt timing-sensitiv sind

Nicht jeder String-Vergleich in einer Anwendung ist ein relevantes Sicherheitsrisiko. Kritisch sind ausschließlich Vergleiche, bei denen ein geheimer Wert gegen eine vom Angreifer wiederholt und mit variierendem Inhalt übermittelte Eingabe geprüft wird, und bei denen die Antwortzeit für den Angreifer messbar ist, etwa über wiederholte API-Aufrufe.

Ein Vergleich zweier öffentlicher Werte, etwa zweier Produktnamen in einer Sortierfunktion, ist nicht timing-sensitiv, weil kein Geheimnis involviert ist. Ebenso wenig relevant sind Vergleiche, bei denen der Angreifer ohnehin keine wiederholten, gezielt variierten Anfragen stellen kann, etwa bei einer strikten Rate-Limitierung mit Sperrung nach wenigen Fehlversuchen. In der Praxis lohnt es sich deshalb, bei jedem Fund eines ===-Vergleichs kurz zu fragen, ob überhaupt ein Geheimnis im Spiel ist, bevor man den Vergleich pauschal durch hash_equals ersetzt.

5. Sonderfall Passwort-Hashes

Beim Vergleich von Passwörtern wird in der Praxis fast nie direkt verglichen, sondern über password_verify, das intern bereits einen konstantzeitigen Vergleich des Hash-Werts durchführt. Wichtig ist trotzdem, dass password_verify ausschließlich für den Vergleich der Hash-Werte zuständig ist, das Hashing selbst mit password_hash und einem geeigneten Algorithmus wie Argon2id erfolgen sollte. Diese Trennung von Hashing und Vergleich ist bewusst so gestaltet, damit Entwickler nicht versehentlich einen eigenen, potenziell unsicheren Vergleichsmechanismus einbauen.

Ein häufiger Fehler ist, ein selbst berechnetes Hash-Ergebnis anschließend mit dem einfachen ===-Operator statt mit hash_equals gegen den gespeicherten Hash zu vergleichen, etwa bei einer eigenen, nicht über password_verify laufenden Prüfung. Auch in diesem Fall ist hash_equals die korrekte Wahl. Das betrifft zum Beispiel eigene Implementierungen von Einmalpasswörtern, Recovery-Codes oder API-Secrets, die häufig außerhalb des klassischen Passwort-Flows verwaltet werden und deshalb leicht übersehen werden.

6. Constant-Time-Vergleiche in anderen Kontexten

Das Prinzip konstantzeitiger Vergleiche ist nicht auf PHP beschränkt. Nahezu jede moderne Sprache bietet eine vergleichbare Funktion an, etwa hmac.compare_digest in Python oder crypto.timingSafeEqual in Node.js. Bibliotheken für JWT-Validierung, OAuth-Signaturen oder Webhook-Verifizierung nutzen diese Funktionen intern meist bereits automatisch, sodass Entwickler in der Regel gar nicht selbst darüber nachdenken müssen, solange sie auf eine etablierte Bibliothek statt auf eine eigene Implementierung setzen.

Problematisch wird es vor allem dann, wenn Entwickler eine eigene, vermeintlich einfachere Prüfung um eine solche Bibliothek herum bauen, etwa einen zusätzlichen manuellen Vergleich vor dem eigentlichen Bibliotheksaufruf, und dabei versehentlich wieder einen nicht konstantzeitigen Vergleich einführen. Ein Code-Review sollte deshalb nicht nur den Einsatz einer sicheren Bibliothek prüfen, sondern auch, ob drumherum keine zusätzliche, unsichere Prüfung existiert.

7. Wie praktisch ausnutzbar sind Timing-Attacken wirklich?

In lokalen Netzwerken mit sehr geringer und stabiler Latenz sind Timing-Attacken gut dokumentiert und praktisch durchführbar. Ueber das offene Internet erschweren Netzwerk-Jitter, Load Balancer und variable Serverlast die Messung erheblich, weshalb ein einzelner Messwert kaum aussagekräftig ist. Angreifer können diese Störungen aber teilweise gezielt reduzieren, indem sie viele parallele Messungen durchführen und statistische Ausreisser herausfiltern, was den praktischen Aufwand senkt, aber nicht vollständig beseitigt.

Mit statistischen Methoden und einer sehr großen Anzahl an Messungen pro Byte lassen sich diese Störungen jedoch großteils herausrechnen, wie mehrere wissenschaftliche Arbeiten und praktische Proof-of-Concept-Angriffe gegen reale Webanwendungen gezeigt haben. Die Absicherung mit hash_equals ist deshalb keine theoretische Vorsichtsmaßnahme, sondern eine mit vertretbarem Aufwand umsetzbare, wirksame Gegenmaßnahme.

8. Häufige Fehler im Umgang mit sicherheitskritischen Vergleichen

Ein häufiger Fehler ist, hash_equals nur an einer Stelle im Code einzusetzen, etwa bei der Haupt-Signaturprüfung, während an anderer Stelle, etwa bei einem zusätzlichen internen Debug- oder Testmodus, weiterhin ein einfacher ===-Vergleich für denselben geheimen Wert verwendet wird.

Ein weiterer Fehler ist, die Reihenfolge der Argumente bei hash_equals zu verwechseln oder einen der beiden Werte vorher ungeprüft aus Nutzereingaben zu übernehmen, ohne Länge oder Typ zu validieren, was in seltenen Fällen selbst wieder zu Seiteneffekten führen kann. Ebenfalls unterschätzt wird, dass hash_equals einen String und keinen beliebigen Wert erwartet, weshalb ein vorheriger Typ-Check gerade bei dynamisch typisierten Eingaben aus JSON-Bodies sinnvoll ist.

9. Best Practices und Checkliste

Jeder Vergleich zwischen einem geheimen, serverseitig bekannten Wert und einer vom Client übermittelten Eingabe sollte konsequent mit hash_equals statt mit dem Standardvergleichsoperator erfolgen, insbesondere bei API-Keys, HMAC-Signaturen und Token-Prüfungen außerhalb von password_verify.

Ergänzend hilft ein Code-Review-Standard, der gezielt nach ===-Vergleichen mit geheimen Werten sucht, sowie eine allgemeine Rate-Limitierung sicherheitskritischer Endpunkte, die zusätzlich zur konstantzeitigen Prüfung die Anzahl möglicher Messungen für einen Angreifer stark einschränkt.

Vergleichstyp Beispiel Timing-Risiko Empfohlene Lösung
API-Key-Prüfung Header X-Api-Key gegen gespeicherten Wert Hoch bei direktem === hash_equals()
HMAC-Signatur Webhook-Payload-Signatur Hoch bei direktem === hash_equals()
Passwort-Hash Login-Prüfung Bereits konstant password_verify()
Oeffentlicher Wert Sortierung von Produktnamen Kein Risiko Normaler ===-Vergleich ausreichend

Mironsoft

Security-Audits, OWASP-konforme Härtung und sichere Architektur

Anwendungen, die einem echten Angriffsversuch tatsächlich standhalten?

Wir prüfen bestehende Anwendungen auf klassische OWASP-Schwachstellen, unsichere Authentifizierung und fehlende Input-Validierung und bauen daraus eine Architektur, die Angriffsflächen strukturell reduziert statt nur einzelne Symptome zu flicken.

Security-Audit

OWASP Top 10, Auth-Flows und Input-Validierung systematisch auf Schwachstellen prüfen.

Sichere Architektur

Rate-Limiting, Verschlüsselung und Zugriffskontrollen von Grund auf richtig aufbauen.

Incident-Vorbereitung

Logging, Monitoring und Reaktionsprozesse für den Ernstfall etablieren.

10. Zusammenfassung

Timing Attacks

Ursache

Byteweiser Vergleich bricht bei erster Abweichung ab.

Erkennung

Statistische Zeitmessung wiederholter Anfragen mit variiertem Wert.

Fix

hash_equals() für alle sicherheitskritischen Vergleiche.

Prävention

Code-Review auf ===-Vergleiche mit geheimen Werten, Rate-Limiting.

11. FAQ: Timing Attacks

1Was ist eine Timing-Attacke?
Eine Timing-Attacke nutzt messbare Unterschiede in der Ausführungszeit eines Vergleichs aus, um Rückschlüsse auf einen geheimen Wert zu ziehen, obwohl das sichtbare Ergebnis des Vergleichs identisch aussieht.
2Warum ist der ===-Operator in PHP timing-sensitiv?
Weil der zugrunde liegende byteweise Vergleich beim ersten abweichenden Byte abbricht, wodurch die Ausführungszeit minimal von der Anzahl übereinstimmender Anfangsbytes abhängt.
3Was macht hash_equals anders?
hash_equals vergleicht immer alle Bytes beider Werte und verknüpft die Ergebnisse bitweise, wodurch die Ausführungszeit ausschließlich von der Länge, nicht vom Inhalt der Werte abhängt.
4Ist jeder String-Vergleich in einer Anwendung ein Risiko?
Nein, nur Vergleiche zwischen einem geheimen Wert und einer vom Angreifer wiederholt variierbaren Eingabe sind relevant. Vergleiche zwischen zwei öffentlichen Werten sind unkritisch.
5Muss man Passwortvergleiche manuell mit hash_equals absichern?
In der Regel nicht, da password_verify intern bereits einen konstantzeitigen Vergleich durchführt. Nur bei eigenen, nicht über password_verify laufenden Prüfungen ist hash_equals explizit erforderlich.
6Sind Timing-Attacken über das offene Internet praktisch durchführbar?
Netzwerk-Jitter erschwert einzelne Messungen erheblich, mit statistischen Methoden und ausreichend vielen Messungen wurden solche Angriffe aber bereits gegen reale Anwendungen erfolgreich demonstriert.
7Gibt es ähnliche Funktionen in anderen Sprachen?
Ja, etwa hmac.compare_digest in Python oder crypto.timingSafeEqual in Node.js. Das Prinzip konstantzeitiger Vergleiche ist sprachübergreifend gleich.
8Wie findet man gefährdete Stellen im eigenen Code?
Durch gezielte Suche nach ===-Vergleichen, bei denen mindestens einer der beiden Werte ein geheimer, sicherheitsrelevanter Wert wie ein API-Key oder eine Signatur ist.
9Hilft Rate-Limiting gegen Timing-Attacken?
Rate-Limiting reduziert die Anzahl möglicher Messungen für einen Angreifer erheblich und ist eine sinnvolle Ergänzung, ersetzt aber nicht die konstantzeitige Vergleichsfunktion selbst.
10Muss hash_equals bei jedem HMAC-Vergleich verwendet werden?
Ja, überall dort, wo eine serverseitig berechnete HMAC-Signatur gegen eine vom Client übermittelte Signatur geprüft wird, sollte konsequent hash_equals statt eines einfachen Vergleichs verwendet werden.