· 7 min read

Securing Telegram Mini Apps Against Forged Init Data

Telegram Mini Apps make it easy to move from bot chat to rich app flows, but the handoff introduces a critical trust boundary: the data Telegram sends to your web app. This tutorial shows how to verify `initData` correctly, avoid common implementation mistakes, and design your app so backend actions never trust the client blindly.

Telegram Mini Apps are web apps launched from Telegram chats, and the user experience is excellent: a user taps a button, your app opens inside Telegram, and you can immediately personalize the interface. That convenience also creates a security boundary you must treat seriously.

The most important rule is simple:

Never trust data coming from the Mini App client unless your backend has verified it.

The primary trust signal for a Telegram Mini App is the initData payload. If you use it correctly, you can safely identify the user, the chat context, and the launch metadata. If you use it incorrectly, attackers can forge requests, impersonate users, or trigger privileged actions in your backend.

This tutorial focuses on the most common and most dangerous mistake: accepting client-provided identity without verifying Telegram’s signature.


What initData is and why it matters

When Telegram opens your Mini App, it passes a signed string of launch parameters to the web app. In the browser, you can access it through the Telegram Web App object as initData or initDataUnsafe.

  • initData is the raw signed payload.
  • initDataUnsafe is a parsed object for convenience.

The key security point is that only the raw initData can be verified. The parsed object is for display and convenience, not for trust.

If your backend receives an authenticated request from the Mini App, it should require the client to send initData alongside the request and then verify it on the server before doing anything sensitive.

Typical sensitive actions include:

  • creating or updating user records
  • issuing internal credits or purchases
  • granting access to paid content
  • changing account settings
  • binding Telegram identities to your app accounts

The attack you are defending against

A common insecure pattern looks like this:

  1. The frontend reads initDataUnsafe.user.id.
  2. The frontend sends that user ID to the backend.
  3. The backend trusts the user ID and performs actions.

This is vulnerable because the client controls what it sends. A modified browser, injected script, or proxy can replace the user ID with any value.

If your backend says, “User 123 requested a premium credit,” an attacker can simply claim to be User 123.

The secure approach is:

  1. The frontend sends the raw initData string.
  2. The backend verifies the signature using your bot token.
  3. Only after verification does the backend extract the Telegram user identity and act on it.

How Telegram signs Mini App launch data

Telegram includes a hash in the launch payload. Your backend reconstructs the data-check string from the received fields, computes the HMAC using a secret derived from your bot token, and compares the result to the supplied hash.

Conceptually:

  1. Parse the query-string-style payload.
  2. Remove the hash field.
  3. Sort remaining key-value pairs alphabetically.
  4. Join them as key=value lines with \n.
  5. Derive the secret key from your bot token.
  6. Compute the HMAC over the data-check string.
  7. Compare the computed hash to the supplied hash.
  8. Reject the request if anything fails.

If the comparison succeeds, you can trust that Telegram generated the payload and that it has not been modified.


Backend verification: the pattern you want

Do verification on the server, not the frontend.

Example Node.js verifier

import crypto from 'crypto';

function verifyTelegramInitData(initData, botToken) {
  const params = new URLSearchParams(initData);
  const hash = params.get('hash');
  if (!hash) return { ok: false, reason: 'missing hash' };

  params.delete('hash');

  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, value]) => `${key}=${value}`)
    .join('\n');

  const secretKey = crypto
    .createHmac('sha256', 'WebAppData')
    .update(botToken)
    .digest();

  const calculatedHash = crypto
    .createHmac('sha256', secretKey)
    .update(dataCheckString)
    .digest('hex');

  const hashBuffer = Buffer.from(hash, 'hex');
  const calcBuffer = Buffer.from(calculatedHash, 'hex');

  if (hashBuffer.length !== calcBuffer.length) {
    return { ok: false, reason: 'hash length mismatch' };
  }

  if (!crypto.timingSafeEqual(hashBuffer, calcBuffer)) {
    return { ok: false, reason: 'invalid signature' };
  }

  const user = params.get('user');
  return { ok: true, userRaw: user ? JSON.parse(user) : null };
}

Important notes about this code

  • It verifies the signature on the backend.
  • It uses a constant-time comparison to reduce timing side-channel risk.
  • It does not trust initDataUnsafe.
  • It still needs additional checks, which we’ll cover next.

If you use a framework, the same logic applies: verify the signed payload first, then parse and authorize.


Do not stop at signature verification

A valid signature only proves that Telegram generated the payload. It does not prove that your app should allow every action.

You still need authorization logic.

Examples:

  • A verified Telegram user may be authentic, but not necessarily premium.
  • A verified user may exist, but not own the resource being edited.
  • A verified group launch may be real, but not grant admin rights.

In other words:

  • Authentication: “Who is this?”
  • Authorization: “What are they allowed to do?”

Treat initData as your authentication input, then apply your own application rules.


Add freshness checks to reduce replay risk

Verified initData can still be replayed if an attacker captures it.

That means you should check the embedded timestamp and reject stale requests.

Telegram includes an auth_date value. A reasonable pattern is:

  • accept only recent payloads, such as within 5–10 minutes for high-risk operations
  • require a new verification step for sensitive actions
  • issue your own server session after successful verification

Use initData once to establish a server-side session, then use your own session token or secure cookie for subsequent API calls.

That gives you:

  • reduced exposure of the raw Telegram payload
  • cleaner application auth flow
  • less repeated verification overhead
  • a place to apply logout, revocation, and idle timeout controls

For especially sensitive actions, re-check freshness or ask the user to re-open the Mini App.


Avoid storing initData as a long-lived credential

A common mistake is to treat initData like a permanent API key.

Don’t do that.

Why:

  • it is a launch assertion, not a reusable secret
  • it may be replayed if leaked
  • it ties you to Telegram-specific verification at every request
  • it makes revocation and session control harder

Instead:

  1. verify initData
  2. create a short-lived server session
  3. issue your own app token or secure session cookie
  4. use that session for normal API traffic

This also makes your app easier to scale across multiple services.


Common implementation mistakes

1. Trusting initDataUnsafe

This is the most common mistake. The object is convenient for rendering UI, but it should not be your source of truth.

2. Verifying on the client only

Any client-side verification can be bypassed because the attacker controls the client.

3. Reconstructing the hash incorrectly

Small mistakes break verification or, worse, create false confidence:

  • wrong field ordering
  • wrong newline handling
  • including the hash field in the data check string
  • using the wrong secret derivation step
  • incorrect character encoding

4. Comparing hashes with ==

Use constant-time comparison on the backend.

5. Skipping timestamp validation

A valid payload from yesterday should not necessarily be accepted today.

6. Using Telegram identity without app-level authorization

A real Telegram user is not automatically allowed to perform every operation in your app.

7. Logging sensitive payloads

initData can contain user-identifying information. Be careful with logs, crash reports, and analytics.


A secure request flow for Mini App APIs

A practical production flow looks like this:

Step 1: Mini App opens

The frontend reads the raw initData string from Telegram.

Step 2: Frontend sends initData to backend

The frontend posts it over HTTPS to a dedicated /auth/telegram endpoint.

Step 3: Backend verifies the signature

If invalid, return 401 Unauthorized.

Step 4: Backend checks freshness

If the payload is too old, return 401 or ask for a fresh launch.

Step 5: Backend maps Telegram identity to your user record

Create or find the local user account.

Step 6: Backend issues a session

Set an HttpOnly, Secure cookie or a short-lived app token.

Step 7: All later API requests use your session

Do not keep sending raw initData unless you need it for re-authentication.

This design keeps the Telegram launch payload as an entry point, not a standing credential.


What to protect in your Mini App architecture

If you are building Telegram Mini Apps at scale, the security concern is rarely the UI itself. It is the backend trust boundary.

Protect these parts carefully:

  • authentication endpoints
  • subscription and payment flows
  • profile mutation routes
  • resource ownership checks
  • admin-only functions
  • coupon, credit, and reward issuance

If any of these accept user identity from the frontend without server verification, they are vulnerable.


Practical checklist

Use this checklist before shipping:

  • [ ] Backend verifies raw initData
  • [ ] Backend ignores initDataUnsafe for trust decisions
  • [ ] HMAC comparison uses constant-time logic
  • [ ] auth_date freshness is enforced
  • [ ] Sensitive actions require app-level authorization
  • [ ] A server session is issued after successful verification
  • [ ] initData is not stored as a long-lived credential
  • [ ] Logs do not leak sensitive launch data
  • [ ] Error responses do not reveal verification internals

Final advice

Telegram Mini Apps are powerful because they collapse discovery, identity, and interaction into one smooth flow. That same convenience can make developers overly trusting.

If you remember only one thing, remember this:

The Mini App frontend is not your security boundary. Your backend is.

Verify initData on the server, check freshness, and then layer your own authorization and session controls on top. If you do that, you can build Mini Apps that are both fast to use and safe to operate.

← All posts