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.
Inhaltsverzeichnis
- 1. Was sind Timing-Attacken?
- 2. Wo das Problem in der Praxis auftritt
- 3. Warum hash_equals() das Problem löst
- 4. Welche Vergleiche überhaupt timing-sensitiv sind
- 5. Sonderfall Passwort-Hashes
- 6. Constant-Time-Vergleiche in anderen Kontexten
- 7. Wie praktisch ausnutzbar sind Timing-Attacken wirklich?
- 8. Häufige Fehler im Umgang mit sicherheitskritischen Vergleichen
- 9. Best Practices und Checkliste
- 10. Zusammenfassung
- 11. FAQ
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.