Zum Inhalt springen
Alle Artikel

Telegram-Mini-App-initData serverseitig validieren

16. Sept. 2026 · Backend · 3 min

·Von Dimitri Pisarev

Alles, was eine Telegram-Mini-App über ihre Nutzerin weiß, kommt in einem Query-String namens initData, signiert von Telegram mit einem HMAC, den das Backend in rund fünfzehn Zeilen verifizieren kann. initDataUnsafe, das bequeme geparste Objekt, ist angreiferkontrolliert und darf nie Grundlage einer Autorisierungsentscheidung sein. Prüfe die Signatur serverseitig bei jeder Anfrage, die Nutzerdaten berührt, und behandle die verifizierte user.id als die einzige Identität, die deine API akzeptiert.

Das ist die gesamte Disziplin hinter Mini-App-Backends, und es ist der Teil eines Shop-Bots ohne Abkürzung: Die Storefront kann wunderschön sein. Akzeptiert der Bestell-Endpunkt eine als JSON verschickte User-ID vom Client, ist jede Person mit curl eine Kundin.

Was signiert Telegram tatsächlich?#

Telegram.WebApp.initData ist ein URL-kodierter Query-String, zum Beispiel:

auth_date=1737000000&query_id=AAHdF6IQAAAAAN0XohDhrOrc&user=%7B%22id%22%3A279058397%7D&hash=47c2b7ec6b...

Der hash-Parameter ist die Signatur über alle anderen Parameter. Der Verifikationsalgorithmus laut Mini-Apps-Spezifikation, wörtlich:

  1. Query-String parsen, hash herausnehmen, merken.
  2. Die übrigen Felder alphabetisch sortieren und als key=value-Zeilen mit \n verbinden.
  3. secret_key = HMAC_SHA256(key="WebAppData", message=bot_token) berechnen.
  4. hex(HMAC_SHA256(key=secret_key, message=data_check_string)) berechnen.
  5. Mit dem gelieferten hash per Constant-Time-Vergleich abgleichen.
import { createHmac, timingSafeEqual } from "node:crypto";
 
export function validateInitData(initData: string, botToken: string): boolean {
  const params = new URLSearchParams(initData);
  const hash = params.get("hash") ?? "";
  params.delete("hash");
 
  const checkString = [...params.entries()]
    .sort(([a], [b]) => (a < b ? -1 : 1))
    .map(([k, v]) => `${k}=${v}`)
    .join("\n");
 
  const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
  const calc = createHmac("sha256", secret).update(checkString).digest();
 
  const given = Buffer.from(hash, "hex");
  return given.length === calc.length && timingSafeEqual(given, calc);
}

Danach ist params.get("user") ein JSON-kodiertes Nutzerobjekt; parsen, und seine id wird die Identität der Anfrage. Alles, was die Mini-App jenseits der Identität braucht (Warenkorb, Preise), wird serverseitig neu abgeleitet, nie vom Client zurückgelesen.

Wie frisch ist frisch? Die Frage, die die Doku offenlässt#

Die Doku sagt, man „kann zusätzlich“ auth_date prüfen und hört dann auf: Ein empfohlenes Fenster fehlt, ein Default ebenso. Das Schweigen ist eine Entscheidung, die man treffen muss. Jede Verifikation beweist bisher „dieser String wurde irgendwann von Telegram signiert“, nicht „diese Session ist aktuell“. Eine praktische Decke sind 24 Stunden, so lange hält eine Mini-App-Session sinnvoll; ein Bestell-Endpunkt darf mehr verlangen. Das Fenster an einer Stelle festlegen und testen. Ein zu enges Fenster loggt Nutzer mitten im Warenkorb aus und sieht wie ein Bug der App aus, nicht wie eine Sicherheitsentscheidung.

Note

Das user-Feld in initData ist selbst JSON, das in einen Query-String-Wert serialisiert wurde. Erst den Query-String dekodieren, dann JSON.parse auf das Nutzerfeld: In der falschen Reihenfolge ist die häufigste Meldung „meine Validierung schlägt immer fehl“.

Was die 10.2-Domain-Sperre abdeckt und was nicht#

Seit Bot API 10.2 (durchgesetzt ab 2026-07-20) verweigert Mini-App-JavaScript die Ausführung außerhalb der in BotFather konfigurierten Origin. Diese Sperre stoppt jemanden, der eine Kopie deines Frontends hostet, um darüber initData zu ernten. Für deine API tut sie nichts: Eine gefälschte Anfrage führt dein JavaScript nie aus. Die Domain-Sperre schützt den Client, die HMAC-Prüfung den Server. Man braucht beide, und für einen Angreifer ist genau eine davon optional.

Die Fehlerfälle, die sich einmal testen lohnen#

  1. Manipuliertes Feld. Die user-ID in initData ändern, den hash behalten: Die Validierung muss fehlschlagen. Klappt sie, wird der Check-String aus den falschen Feldern gebaut.
  2. Replay. Ein abgefangenes gültiges initData nächste Woche wieder abgesendet: besteht den HMAC, scheitert an einem vernünftigen auth_date-Fenster. Genau dieser Check wird am häufigsten weggelassen.
  3. Falsche Sortierung. Den Check-String unsortiert bauen: schlägt gegen Telegrams eigene gültige Daten fehl, was Leute glauben lässt, der Algorithmus sei falsch und nicht der Sort. Ist er nicht.
  4. Der Rechnung des Clients vertrauen. Eine gültige Identität mit price: 1 im Body, wo der Katalog 8,90 sagt: Der HMAC ist hier irrelevant. Preise, Summen und Produkt-IDs sind Server-Fakten; die Mini-App schickt Absicht, keine Zahlen.

Fünfzehn Zeilen Krypto, ein Frische-Fenster und die Regel, dass clientgeschickte Zahlen Vorschläge sind: Das ist das gesamte Sicherheitsmodell eines Mini-App-Backends, und es hält dicht, wenn es durchgesetzt wird.