← All posts

Die SAML-Signatur war gültig. Das war nie das Problem.

Authagonal·July 16, 2026
samlssosecurity

SAML Single Sign-On hat ein Replay-Problem, das in seiner Struktur angelegt ist, und die meisten Service Provider "lösen" es genau an der einen Stelle, die ein Angreifer erreichen kann.

So sieht der Ablauf aus. Du bist der Service Provider. Ein Nutzer landet auf deiner Login-Seite, du schickst einen AuthnRequest an seinen Identity Provider, und diese Anfrage trägt eine zufällige ID. Der IdP authentifiziert den Nutzer und schickt eine SAML Response zurück, mit InResponseTo="<that ID>". Du schlägst die ID nach, bestätigst, dass du sie tatsächlich ausgestellt hast, und verbrauchst sie, damit sie nie zweimal verwendet werden kann. Anfrage an Antwort gebunden, Antwort einmal genutzt, Replay abgewehrt. Lehrbuch.

Jetzt die Frage, die das Ganze zum Einsturz bringt: Was hat der IdP eigentlich signiert?

Nicht die Response. Die Assertion. Entra, Okta, die meisten Anbieter im Feld signieren das <Assertion>-Element und lassen den umgebenden <Response>-Umschlag unsigniert. Das ist normal, und die Spezifikation erlaubt es. Aber InResponseTo sitzt auf der <Response>. Das Feld also, an dem deine gesamte Replay-Abwehr hängt, ist genau das eine Feld, das die Signatur nicht abdeckt. Du kannst es bearbeiten, löschen, damit machen, was du willst, und die Signatur der Assertion validiert trotzdem einwandfrei, weil du die Assertion nie angefasst hast.

Sieh dir also an, was passiert, wenn ein Angreifer das Naheliegende tut.

SAML gibt es in zwei Varianten. SP-initiierte Responses beantworten eine Anfrage, die du geschickt hast, also tragen sie ein InResponseTo. IdP-initiierte (unaufgeforderte) Responses wurden nie angefordert, also haben sie überhaupt kein InResponseTo. Dein Code verzweigt darauf, ganz vernünftig: Gibt es ein InResponseTo, gleiche es mit einer gespeicherten Anfrage ab und verbrauche sie; gibt es keines, gibt es auch keine Anfrage zum Abgleichen, also überspringt er diese Prüfung. Genau dieses Überspringen ist die ganze Schwachstelle.

Der Angriff besteht aus drei Schritten und null Kryptografie:

  1. Schneide eine echte, erfolgreiche SAML Response mit. Ein echter Login, eine echte Signatur, eine echte Assertion.
  2. Lösche das InResponseTo-Attribut. Die Signatur deckt die Assertion ab, nicht den Umschlag, also verifiziert sie weiterhin.
  3. Schick sie zurück. Ohne InResponseTo behandelt dein SP sie als IdP-initiiert, nimmt den Zweig ohne Replay-Prüfung, validiert die Signatur (gültig) und meldet den Nutzer an.

Und dann schick sie noch einmal. Und noch einmal. Die Assertion ist jetzt ein Generalschlüssel. Jede Prüfung, auf die du stolz warst, ist weiterhin erfolgreich: Signatur gültig, Zertifikat gepinnt, Aussteller korrekt, Bedingungen im gültigen Zeitraum. Die eine Kontrolle, die einen Replay gestoppt hätte, hing an einem Feld, das der Angreifer gratis löscht.

Hier die Falle in einem Satz: Du hast die Assertion signiert, aber deine Prüfung hing am Umschlag.

Die Lösung ist nicht mehr Validierung. Es ist, das Richtige zu validieren. Häng den Replay-Schutz nicht mehr an InResponseTo, das unsigniert und optional ist, sondern an die ID der Assertion selbst, die signiert und immer vorhanden ist. Jede Assertion hat eine ID, sie liegt innerhalb der Signatur, und jede Manipulation daran bricht die Verifikation. Führe einen Einmal-Cache von Assertion-IDs, begrenzt durch das NotOnOrAfter jeder Assertion, damit der Cache nie unbegrenzt wächst, und weise jede ID ab, die du schon einmal gesehen hast. Jetzt bringt dem Angreifer das Löschen von InResponseTo nichts mehr, weil der Wert, den du tatsächlich prüfst, weder gefälscht noch wiederholt werden kann.

Wenn du schon dabei bist, behandle die Bindung an die Anfrage als Defense in Depth und nicht als primäre Kontrolle. Gleiche InResponseTo weiterhin ab, wenn es vorhanden ist. Weise unaufgeforderte Responses weiterhin rundheraus ab, wenn du IdP-initiierten Login nie anbietest. Erzwinge weiterhin Audience und das Conditions-Zeitfenster. Aber die tragende Anti-Replay-Prüfung muss auf signierten Bytes sitzen, Punkt.

Wir kennen diese hier, weil wir sie beim Audit unseres eigenen SAML vor dem Launch gefunden haben, nicht weil ein Kunde sie hinterher entdeckt hat. Und die Lehre lässt sich weit über SAML hinaus verallgemeinern. Eine Signatur beantwortet genau eine Frage: Hat der Inhaber dieses Schlüssels diese Bytes erzeugt. Sie sagt dir nicht, dass die Bytes frisch sind, dass sie für dich bestimmt waren oder dass du sie nicht schon einmal akzeptiert hast. Das sind drei getrennte Prüfungen, und jede einzelne davon muss ein Feld lesen, das die Signatur tatsächlich abdeckt. In dem Moment, in dem eine Sicherheitsentscheidung von Daten abhängt, die außerhalb der Signatur liegen, ist sie keine Sicherheitsentscheidung mehr, sondern eine höfliche Bitte, die ein Angreifer nach Belieben ablehnen darf.

Signiere das Feld, an dem du prüfst, oder prüfe an dem Feld, das signiert ist. Eine dritte Option, die einen Replay übersteht, gibt es nicht.

Wenn du SAML-Validierung lieber nicht selbst zusammenbaust und dabei den Replay-Fall falsch machst, ist das ein guter Grund, sie jemand anderen betreiben zu lassen. Authagonal hängt den Replay-Schutz an die signierte Assertion-ID, sodass das Entfernen von InResponseTo ins Leere läuft.