CVE-2026-90848: Ein fehlendes Escaping in der AusweisApp und seine Folgen
Eine ungeprüfte Fehlermeldung des eID-Servers führte in der AusweisApp 2.5.4 zu XSS, manipulierten Dialogen und Abstürzen. Ursache, Angriff und Fix.

Die AusweisApp ist der offizielle eID-Client des Bundes, mit dem sich Bürgerinnen und Bürger online mit dem Personalausweis ausweisen. Wir haben Version 2.5.4 untersucht: statische Analyse, rund 80 Millionen Fuzzing-Durchläufe unter AddressSanitizer und Live-Angriffe mit einem eigenen, bösartigen eID-Server. Die Angriffe haben wir zuerst in einer Container-Umgebung durchgespielt und anschließend auf echter Hardware verifiziert.
Der Kern der App hat gehalten. Kryptografie, EAC-Zustandsmaschine, Herkunftsprüfung und die Parser für Karten- und Serverdaten haben keinem Angriff nachgegeben. Gefunden haben wir eine einzige Ursache: Eine Fehlermeldung des eID-Servers wurde ungeprüft als HTML weitergegeben. Daraus folgten JavaScript im Browser des Opfers (CVE-2026-90848, nach unserer Bewertung CVSS 3.1 6.1, unser Advisory TP-2026-113), ein manipulierbarer App-Dialog mit Ortungssignal ohne Klick und ein Absturz über die mitgelieferte Qt-SVG-Bibliothek. Wir haben die Lücke gefunden und an Governikus gemeldet, behoben ist sie in 2.5.5 und 2.6.0. Wer die AusweisApp nutzt, sollte auf 2.6.0 oder neuer aktualisieren.
Warum eine Desktop-App eine Webanwendung ist
Die AusweisApp ist ein nativer C++/Qt-Client ohne WebView und ohne JavaScript-Engine. Klassisches XSS scheint damit ausgeschlossen. Der Eindruck täuscht. Nach BSI TR-03124 startet eine Webseite die Online-Ausweisfunktion, indem sie den Browser auf einen lokalen Dienst der App schickt:
http://127.0.0.1:24727/eID-Client?tcTokenURL=https://dienstanbieter.example/token
Die App holt das TC-Token, verbindet sich mit dem darin genannten eID-Server und spricht mit ihm das SOAP-basierte PAOS-Protokoll. Am Ende leitet sie den Browser zurück oder liefert ihm eine HTML-Fehlerseite. Zwei Punkte sind entscheidend: Der Client dieses Dienstes ist der Browser des Opfers, und den eID-Server bestimmt die aufrufende Webseite frei über tcTokenURL.

Angreifermodell: ein eigener eID-Server reicht
Ein echter eID-Server braucht ein vom BSI kontrolliertes Berechtigungszertifikat (CVC), sonst gibt der Ausweis keine Daten heraus. Diese Sicherung funktioniert. Interessant ist, was vor ihrer Prüfung passiert.
Die Bindung des TLS-Zertifikats an das Berechtigungszertifikat greift erst, wenn beide vorliegen (CertificateChecker.cpp:70-72). Bei der ersten Servernachricht ist das nicht der Fall, ein gewöhnliches Let's-Encrypt-Zertifikat genügt. Diese erste Nachricht darf ein Fehler sein: Die App sendet StartPAOS, der Server antwortet mit einem Fehlerergebnis, und die Zustandsmaschine springt ohne Kartenkontakt, PACE oder Zertifikatsprüfung in den Fehlerpfad. Den Fehlertext liefert das frei wählbare Feld ResultMessage.
Der Angreifer braucht also eine Domain mit TLS-Zertifikat, einen HTTPS-Server und einen Link, den das Opfer öffnet. Ausweis, PIN, Berechtigungszertifikat oder eine Position im Netzwerk braucht er nicht.
Die Ursache: ein fehlendes toHtmlEscaped()
Die App weiß, dass Servertexte gefährlich sein können. Für 35 Fehlercodes zeigt sie deshalb einen eigenen Text statt der Servermeldung (GlobalStatus::isMessageMasked()). Ein Code fehlt in der Liste: Paos_Generic_Server_Error, den der Server über die Fehlerunterart dp#communicationError auslöst. Für ihn gibt die App den Servertext direkt zurück, und von dort fließt er unmaskiert an zwei Stellen weiter: in die HTML-Fehlerseite für den Browser und in den Qt-Dialog der App.
Wie schmal die Lücke ist, zeigt AuthModel.cpp. Die Datei maskiert in Zeile 251 die Transaktionsinformation korrekt, in Zeile 147 die Servermeldung nicht:
// src/ui/qml/AuthModel.cpp:251 (v2.5.4), korrekt
didAuthenticateEAC1->getTransactionInfo().toHtmlEscaped();
// src/ui/qml/AuthModel.cpp:147 (v2.5.4), fehlt
errorDescription += QStringLiteral("<br/>(%1)").arg(externalInfo);
Wirkung 1: JavaScript im Ursprung der AusweisApp
Der Angreifer setzt die RefreshAddress im TC-Token auf einen fremden Ursprung. Die Prüfung auf denselben Ursprung scheitert, und die App antwortet dem Browser mit einer Fehlerseite. Die Vorlagen-Engine (Template.cpp) setzt die Fehlerbeschreibung unverändert ein. Die Antwort des Angreifer-Servers:
<StartPAOSResponse>
<Result>
<ResultMajor>http://www.bsi.bund.de/ecard/api/1.1/resultmajor#error</ResultMajor>
<ResultMinor>http://www.bsi.bund.de/ecard/api/1.1/resultminor/dp#communicationError</ResultMinor>
<ResultMessage><script>document.title='XSSPROOF'</script></ResultMessage>
</Result>
</StartPAOSResponse>
Der XML-Parser löst die Entitäten auf, die App liefert HTTP 400 mit Content-Type: text/html, und das Skript läuft im Ursprung http://127.0.0.1:24727.
Die Wirkung wollen wir nicht größer machen, als sie ist. Der Ursprung hat keine Cookies und keine Sitzung, lesende Anfragen an den Dienst waren dank Access-Control-Allow-Origin: * ohnehin möglich. Der Gewinn für den Angreifer ist Vertrauen: In der Adressleiste steht die lokale Ausweis-App des Bundes, nicht seine Domain. Eine Aufforderung, dort die PIN „zur Bestätigung“ einzugeben oder Software herunterzuladen, wirkt amtlich. Die Details stehen in unserem Advisory TP-2026-113.
Wirkung 2: manipulierter App-Dialog und Ortungssignal ohne Klick
Derselbe Text erreicht den Ergebnisdialog der App. Dort läuft kein JavaScript, aber Qt-Textelemente verstehen eine HTML-Untermenge, und die Textkomponente GText steht auf Text.AutoText. Mit einer ResultMessage mit Fettdruck, Link und eingebettetem Bild meldete unser Aufzeichnungsserver etwa 67 Millisekunden nach der Serverantwort:
[BEACON-HIT] GET /beacon-resultmsg.png UA=Mozilla/5.0
Niemand hatte geklickt. Der Angreifer erfährt so IP-Adresse und Zeitpunkt und dass die AusweisApp gerade benutzt wird. Darstellen kann er Fettdruck, Farben, Listen, Links und Bilder, auch als base64-kodiertes SVG. Eine täuschend echte Erfolgsmeldung gelingt dagegen nicht: Überschrift „Authentifizierung fehlgeschlagen“, rotes Fehlersymbol und Präfix der App bleiben sichtbar. Darunter kann aber grüner Text mit Haken und „Verifiziert“ stehen. Wer nur darauf schaut und auf den Link klickt, fällt trotzdem herein.
Links öffnet die App mit Qt.openUrlExternally(). Auf Android und iOS ist sie selbst Handler für eid://, ein eingeschleuster Link startet dort also mit einem Fingertipp einen neuen Ausweisvorgang zum Angreifer. Belegt haben wir das im Quelltext, live nur den Umweg über den Browser auf dem Desktop.
Wirkung 3: Absturz über die gebündelte Qt-SVG-Bibliothek
Version 2.5.4 bündelt Qt 6.10.2. Diese Version ist von CVE-2026-6210 in Qt SVG betroffen. Der eingeschleuste img-Tag ist der einzige Weg, auf dem ein Angreifer der App ein SVG unterschieben kann. Unser PoC, ein line-Element, das als Marker auf sich selbst verweist, löst eine Endlosrekursion bis zur Stack Guard Page aus (SIGSEGV). Bestätigt haben wir den Absturz an der Bibliothek in Version 6.10.2 und auf echter Hardware. Ein Schreibprimitiv liefert dieser Pfad nicht, Codeausführung über diesen Weg schließen wir aus.
Was gehalten hat
Kein Angriff konnte aus der Ferne Personendaten vom Ausweis lesen. Diese Grenze zieht der Chip, nicht die App. Die EAC-Zustandsmaschine lässt keinen Schritt überspringen oder umstellen, angefragte Rechte werden gegen das Berechtigungszertifikat geschnitten. Rund 80 Millionen Fuzzing-Durchläufe unter AddressSanitizer, von den Parsern für CVC, ASN.1, PAOS-XML und Kartendateien bis zu den Bildformaten, fanden keinen Speicherfehler auf einem im Standardbau erreichbaren Pfad. Die Herkunftsbindung nach TR-03124 hielt 45 Varianten stand, unter anderem mit Punycode, IPv6-Schreibweisen, Homoglyphen und Steuerzeichen. Request Smuggling, XXE und Parameter-Einschleusung über die Kommandozeile waren nicht möglich, und der Updater pinnt die Zertifikate der Governikus-Server.
Behebung durch Governikus
Governikus hat schnell reagiert. Version 2.5.5 erschien am 14. August 2026, sieben Tage nach unserer Meldung, und maskiert in der Vorlagen-Engine jeden Wert:
// src/network/Template.cpp
- mContext[pKey] = pValue;
+ mContext[pKey] = pValue.toHtmlEscaped();
Version 2.6.0 vom 6. Oktober 2026 maskiert die externe Information zusätzlich an der Quelle in GlobalStatus.cpp und schützt damit auch den App-Dialog vor der ResultMessage. Dazu kamen Qt 6.11.1, das CVE-2026-6210 schließt, und eine zeitliche Begrenzung des Kopplungscodes für „Smartphone als Kartenleser“. Governikus hat damit auch Wirkungen behoben, die nicht in unserer Meldung standen.
Was wir daraus mitnehmen
Unsere ersten Runden hatten XSS ausgeschlossen. Die Begründung klang solide: Alle Stellen, die die Anfrage-URL in HTML zurückgeben, sind durch Qts URL-Kodierung geschützt, und im Quellbaum gibt es kein WebView. Beides stimmte, der Schluss war trotzdem falsch. Die Serverantwort lief auf einem anderen Weg in dieselbe Vorlage, und die App braucht keinen Browser, weil sie ihr HTML an den Browser des Nutzers ausliefert.
Daraus folgen zwei Regeln für jeden Penetrationstest und jedes Code-Review. Jeder lokale HTTP-Dienst, der text/html ausliefert, ist eine Webanwendung, egal in welcher Sprache er geschrieben ist. Und wer einen unmaskierten Wert findet, sollte nicht fragen, was er an dieser Stelle anrichten kann, sondern wohin er sonst noch fließt. Das OWASP XSS Prevention Cheat Sheet gilt deshalb auch für native Clients.
Zeitleiste
| Datum | Ereignis |
|---|---|
| 07.08.2026 | Meldung an Governikus (PGP) |
| 14.08.2026 | AusweisApp 2.5.5 behebt die Browser-Fehlerseite |
| 15.09.2026 | CVE-2026-90848 veröffentlicht |
| 06.10.2026 | AusweisApp 2.6.0 behebt den App-Dialog und aktualisiert Qt auf 6.11.1 |
| 20.10.2026 | Veröffentlichung dieses Beitrags |