24 β Web App xavfsizligi: initData¶
β¬ οΈ Oldingi: 23 β Telegram Web App (Mini App) asoslari Β· π README Β· Keyingi: 25 β Mini App backend β‘οΈ
Bu bobda: Mini App'ning eng muhim, lekin ko'pincha e'tibordan chetda qoladigan tomoni β xavfsizlikni o'rganamiz. Avval nega clientga umuman ishonmaslik kerakligini ko'ramiz: brauzerdan kelgan har qanday "men user 42man" degan da'voni har kim
curlbilan soxtalashtira oladi. Keyin Telegram bizga shu muammoni yechib beradigan vositani βinitDatani (imzolangan, ishonchli ma'lumot:user,auth_date,query_idvahash) tahlil qilamiz. So'ng validatsiya algoritmini bosqichma-bosqich quramiz:hashsiz maydonlarni saralab\nbilan ulabdata_check_string,secret_key = HMAC_SHA256(b"WebAppData", token),calculated_hash = HMAC_SHA256(secret_key, data_check_string)vahmac.compare_digestbilan xavfsiz solishtirish. Replay-hujumga qarshiauth_dateeskirishini ham tekshiramiz. Bularning hammasini aiogram tayyor beradi βfrom aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_dataβ lekin biz avval qo'lda yozamiz (algoritmni tushunish uchun), keyin tayyor util'ga o'tamiz. Oxirida FastAPIDependsorqali real autentifikatsiya qatlami quramiz.Halol eslatma: Bu bobdagi BUTUN validatsiya mantig'i β qo'lda HMAC algoritmi, aiogram'ning
check_webapp_signature/safe_parse_webapp_init_datautil'lari,pytesttestlari va FastAPITestClientauth-endpointi β soxta token (123456:AAH-Test_abc) bilan, internetsiz, HAQIQATAN ishga tushirib tekshirilgan: to'g'ri imzo qabul qilinadi, bitta belgi buzilsa rad etiladi, noto'g'ri token rad etiladi, eskirganauth_daterad etiladi. RUN qilib bo'lmaydigan yagona qism β jonliinitDatani real qurilmadan olish (Telegram client + HTTPS hosting kerak); u illustrativ deb belgilangan, lekin uni tekshiradigan kod aynan shu β to'liq ishlaydi.
24.1 β Nega clientga ishonmaslik kerak?¶
01β18 boblarda bot mantig'i serverda ishladi β foydalanuvchi faqat tugma bosdi yoki xabar yubordi. Mini App boshqacha: u brauzerda (Telegram ichidagi WebView'da) ishlaydigan oddiy veb-sahifa. Brauzerda ishlaydigan har qanday narsa esa β foydalanuvchining (yoki hujumchining) to'liq nazoratida.
Tasavvur qiling, Mini App'ingizda balans bor va frontend backend'ga shunday so'rov yuboradi:
Backend user_idga ishonib, 42-foydalanuvchining balansini qaytaradi. Muammo: bu so'rovni Telegram shart emas. Istalgan odam terminaldan yuboradi:
# Hujumchi o'zini istalgan kim deb tanishtiradi
curl -X POST https://sizning-backend.uz/api/balans \
-H "Content-Type: application/json" \
-d '{"user_id": 1, "amal": "ol"}'
user_id: 1 β admin bo'lishi mumkin. Hujumchi hech qanday parol bilmasa ham, boshqa odamning ma'lumotini o'qib/o'zgartirib oladi. JavaScript'dagi if (user.is_admin) tekshiruvi ham himoya emas β uni brauzer konsolida bir soniyada chetlab o'tish mumkin.
Xulosa β oltin qoida:
Frontend qulaylik uchun. Har qanday muhim qaror serverda, ishonchli ma'lumot asosida qabul qilinadi. "Men user 42man" degan da'voni isbotsiz qabul qilmang.
Savol tug'iladi: agar clientga ishonmasak, foydalanuvchi kimligini qayerdan bilamiz? Mana shu yerda Telegram yordamga keladi.
24.2 β initData nima?¶
Mini App ochilganda Telegram unga maxsus ma'lumotni β initDatani beradi. Bu URL-kodlangan (query-string ko'rinishidagi) satr, ichida:
| Maydon | Mazmuni |
|---|---|
user |
JSON: {"id": ..., "first_name": ..., "username": ...} |
auth_date |
Mini App ochilgan Unix-vaqt (replay tekshiruvi uchun) |
query_id |
Sessiya identifikatori (answerWebAppQuery uchun) |
chat, start_param, ... |
Qo'shimcha kontekst (ixtiyoriy) |
hash |
Yuqoridagilarning hammasidan, bot token'i bilan hisoblangan imzo |
Frontend'da unga shunday murojaat qilinadi (23-bobdagi window.Telegram.WebApp):
// Mini App (frontend) β initData'ni backend'ga yuborish
const initData = window.Telegram.WebApp.initData; // imzolangan satr
fetch("/api/me", {
headers: { "Authorization": "tma " + initData }, // header orqali yuboramiz
});
Eng muhim qism β hash. Uni faqat Telegram (bot token'ini bilgan holda) hisoblay oladi. Bot token'i esa faqat sizda va Telegram'da bor (13-bobda token'ni hech kimga bermaslikni aytgandik). Demak:
- Hujumchi
userni o'zgartirsa (masalanid: 1qilib),hashendi mos kelmaydi β token'siz to'g'rihashhisoblay olmaydi. - Backend
initDatani o'z token'i bilan qayta hisoblab, kelganhashbilan solishtiradi. Mos kelsa β bu ma'lumot HAQIQATAN Telegram'dan, buzilmagan.
Bu simmetrik imzo (HMAC): bir xil kalit bilan imzolanadi va tekshiriladi. Kalit β bot token'idan keltirib chiqariladi.
24.3 β Validatsiya algoritmi (qo'lda)¶
Telegram'ning rasmiy algoritmi (core.telegram.org/bots/webapps#validating-data) to'rt bosqichdan iborat. Avval uni qo'lda yozamiz β faqat standart hmac va hashlib kerak, hech qanday tashqi kutubxonasiz.
1-bosqich. data_check_string. initDatani lug'atga aylantiramiz, hashni ajratib olamiz, qolgan maydonlarni kalit bo'yicha saralab, har birini key=value ko'rinishida \n bilan ulaymiz:
from urllib.parse import parse_qsl
parsed = dict(parse_qsl(init_data, strict_parsing=True))
received_hash = parsed.pop("hash") # hash'ni ajratamiz β u tekshiruvga kirmaydi
data_check_string = "\n".join(f"{k}={parsed[k]}" for k in sorted(parsed))
Saralash MUHIM β Telegram aynan saralangan tartibda imzolaydi. Tartib buzilsa, hash mos kelmaydi.
2-bosqich. secret_key. Bot token'idan sirli kalit chiqaramiz. Diqqat: bu yerda kalit b"WebAppData", xabar esa token (boshqa Telegram imzolarida teskari bo'ladi β adashtirmang):
import hmac, hashlib
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=hashlib.sha256).digest()
3-bosqich. calculated_hash. Endi secret_keyni kalit qilib, data_check_stringni imzolaymiz:
calculated_hash = hmac.new(
key=secret_key,
msg=data_check_string.encode(),
digestmod=hashlib.sha256,
).hexdigest()
4-bosqich. Xavfsiz solishtirish. Hisoblangan hashni kelgan hash bilan solishtiramiz β lekin oddiy == bilan emas, hmac.compare_digest bilan. Bu funksiya vaqt-hujumiga (timing attack) chidamli: solishtirish vaqti satrlar qanchalik mos kelishidan qat'i nazar bir xil bo'ladi, shu sababli hujumchi javob tezligidan hashni "taxminlay" olmaydi:
if not hmac.compare_digest(calculated_hash, received_hash):
raise ValueError("hash mos kelmadi β imzo soxta")
5-bosqich (qo'shimcha, lekin SHART). auth_date eskirishini tekshir. Imzo to'g'ri bo'lsa ham, eski initDatani hujumchi qayta yuborishi mumkin (replay-hujum). Buning oldini olish uchun auth_date juda eski bo'lsa rad etamiz:
import time
auth_date = int(parsed["auth_date"])
if time.time() - auth_date > 86400: # 24 soatdan eski -> rad et
raise ValueError("initData eskirgan (replay?)")
Diqqat: aiogram'ning
check_webapp_signaturefaqat imzoni tekshiradi,auth_dateeskirishini tekshirmaydi. Replay himoyasi β sizning zimmangizda. Buni har doim qo'shing.
To'liq funksiya:
# auth_manual.py β qo'lda validatsiya
import hmac, hashlib, json, time
from urllib.parse import parse_qsl
def validate_init_data(token: str, init_data: str, max_age: int = 86400) -> dict:
parsed = dict(parse_qsl(init_data, strict_parsing=True))
received_hash = parsed.pop("hash", None)
if received_hash is None:
raise ValueError("hash maydoni yo'q")
data_check_string = "\n".join(f"{k}={parsed[k]}" for k in sorted(parsed))
secret_key = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
calc = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(calc, received_hash):
raise ValueError("hash mos kelmadi β imzo soxta")
auth_date = int(parsed.get("auth_date", "0"))
if max_age and (time.time() - auth_date) > max_age:
raise ValueError("initData eskirgan (replay?)")
user = json.loads(parsed["user"]) if "user" in parsed else None
return {"user": user, "auth_date": auth_date}
24.4 β TO'LIQ OFFLINE tekshiruv (token+tarmoq KERAKMAS)¶
Endi eng qiziq qism. Bizda real Telegram qurilmasi yo'q, lekin imzo algoritmini to'liq sinashimiz mumkin: o'zimiz soxta token bilan to'g'ri initData quramiz, keyin uni tekshiramiz. Bu β aynan Telegram qiladigan ishni taqlid qilish.
Diqqat: quyidagi
build_init_datafunksiyasi FAQAT test uchun β u Telegram'ning imzolovchi tarafini taqlid qiladi. Real backend'ingiz HECH QACHONinitDataqurmaydi; u faqat keladiganinitDatani tekshiradi.
# verify_initdata.py β TO'LIQ OFFLINE, internetsiz
import hmac, hashlib, json, time
from urllib.parse import urlencode, parse_qsl
from auth_manual import validate_init_data
FAKE_TOKEN = "123456:AAH-Test_abc" # soxta token
def build_init_data(token, user, auth_date, query_id="AAEtest"):
"""Telegram qiladigan IMZOLASH ishini taqlid qilamiz (faqat test uchun)."""
fields = {
"query_id": query_id,
"user": json.dumps(user, separators=(",", ":")),
"auth_date": str(auth_date),
}
dcs = "\n".join(f"{k}={fields[k]}" for k in sorted(fields))
secret = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
fields["hash"] = hmac.new(secret, dcs.encode(), hashlib.sha256).hexdigest()
return urlencode(fields)
user = {"id": 42, "first_name": "Oqil", "username": "i_oqil"}
now = int(time.time())
init_data = build_init_data(FAKE_TOKEN, user, now)
# 1) to'g'ri initData -> qabul, user'ni qaytaradi
res = validate_init_data(FAKE_TOKEN, init_data)
assert res["user"]["id"] == 42
print("1) to'g'ri initData -> OK, user.id =", res["user"]["id"])
# 2) bitta belgini buzamiz -> rad
bad = init_data[:-1] + ("0" if init_data[-1] != "0" else "1")
try:
validate_init_data(FAKE_TOKEN, bad)
raise SystemExit("XATO: buzilgan o'tib ketdi!")
except ValueError as e:
print("2) buzilgan initData -> rad:", e)
# 3) noto'g'ri token -> rad
try:
validate_init_data("999999:WRONG_token_xyz", init_data)
raise SystemExit("XATO: noto'g'ri token o'tdi!")
except ValueError as e:
print("3) noto'g'ri token -> rad:", e)
# 4) eskirgan auth_date -> rad
old = build_init_data(FAKE_TOKEN, user, now - 90000) # ~25 soat oldin
try:
validate_init_data(FAKE_TOKEN, old, max_age=86400)
raise SystemExit("XATO: eskirgan o'tdi!")
except ValueError as e:
print("4) eskirgan auth_date -> rad:", e)
print("HAMMA TEST O'TDI (token+internet ishlatilmadi)")
Ishga tushiramiz:
Haqiqiy natija (RUN qilingan):
1) to'g'ri initData -> OK, user.id = 42
2) buzilgan initData -> rad: hash mos kelmadi β imzo soxta
3) noto'g'ri token -> rad: hash mos kelmadi β imzo soxta
4) eskirgan auth_date -> rad: initData eskirgan (replay?)
HAMMA TEST O'TDI (token+internet ishlatilmadi)
To'rt holatning to'rttasi ham kutilganidek ishladi: to'g'ri imzo qabul, har qanday o'zgartirish (bir belgi ham!) yoki noto'g'ri token rad, eskirgan initData rad. Hujumchi userni o'zgartira olmaydi β chunki token'ni bilmaydi, to'g'ri hash hisoblay olmaydi.
24.5 β aiogram tayyor vositasi¶
Endi qo'lda yozish shart emas β aiogram'da bu logika tayyor. aiogram.utils.web_app ikkita asosiy funksiya beradi:
check_webapp_signature(token, init_data) -> boolβ imzoni tekshiradi,True/Falseqaytaradi.safe_parse_webapp_init_data(token, init_data) -> WebAppInitDataβ imzoni tekshiradi VA ma'lumotni qulay obyektga aylantiradi; imzo noto'g'ri bo'lsaValueErrorko'taradi.
# aiogram util bilan
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
# faqat bool kerak bo'lsa:
if check_webapp_signature(TOKEN, init_data):
print("imzo to'g'ri")
# user'ni ham olish kerak bo'lsa (tavsiya etiladi):
data = safe_parse_webapp_init_data(TOKEN, init_data) # ValueError -> soxta
print(data.user.id, data.user.username, data.auth_date)
safe_parse_webapp_init_data ichida aynan check_webapp_signatureni chaqiradi, keyin ma'lumotni WebAppInitData (Pydantic model)ga o'giradi β data.user WebAppUser, data.auth_date esa datetime bo'ladi. Bu β productionda ishlatadigan yo'l.
Buni ham OFFLINE tekshiramiz (yuqoridagi init_data va bad shu yerda ham ishlatiladi):
# verify_aiogram_util.py β OFFLINE
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
# (init_data, bad, FAKE_TOKEN avvalgidek qurilgan)
assert check_webapp_signature(FAKE_TOKEN, init_data) is True # to'g'ri
assert check_webapp_signature(FAKE_TOKEN, bad) is False # buzilgan
assert check_webapp_signature("999999:WRONG_token_xyz", init_data) is False # noto'g'ri token
data = safe_parse_webapp_init_data(FAKE_TOKEN, init_data)
assert data.user.id == 42
print("safe_parse user:", data.user.id, data.user.username)
try:
safe_parse_webapp_init_data(FAKE_TOKEN, bad)
except ValueError as e:
print("buzilgan -> ValueError:", e)
Haqiqiy natija (RUN qilingan):
B1 check_webapp_signature(to'g'ri) -> True
B2 check_webapp_signature(buzilgan) -> False
B3 check_webapp_signature(noto'g'ri token) -> False
B4 safe_parse user.id = 42 username = i_oqil
B5 safe_parse(buzilgan) -> ValueError: Invalid init data signature
aiogram'ning natijasi qo'lda yozgan kodimiz bilan aynan bir xil β chunki algoritm bitta. Endi qo'lda yozish o'rniga shu util'ni ishlatamiz, lekin ostida nima sodir bo'layotganini bilamiz.
24.6 β pytest bilan avtomatlashtirish¶
16-bobda o'rgangan pytest bilan validatsiyani test paketiga aylantiramiz. Bu β har deploydan oldin avtomatik ishga tushadigan xavfsizlik kafolati.
# test_auth.py
import hmac, hashlib, json, time
from urllib.parse import urlencode
import pytest
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
TOKEN = "123456:AAH-Test_abc"
def build(token, user, auth_date):
fields = {"user": json.dumps(user, separators=(",", ":")), "auth_date": str(auth_date)}
dcs = "\n".join(f"{k}={fields[k]}" for k in sorted(fields))
secret = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
fields["hash"] = hmac.new(secret, dcs.encode(), hashlib.sha256).hexdigest()
return urlencode(fields)
@pytest.fixture
def valid_init_data():
return build(TOKEN, {"id": 7, "first_name": "Test"}, int(time.time()))
def test_valid_signature(valid_init_data):
assert check_webapp_signature(TOKEN, valid_init_data) is True
def test_tampered_rejected(valid_init_data):
bad = valid_init_data[:-1] + ("0" if valid_init_data[-1] != "0" else "1")
assert check_webapp_signature(TOKEN, bad) is False
def test_wrong_token_rejected(valid_init_data):
assert check_webapp_signature("000:WRONG", valid_init_data) is False
def test_safe_parse_user(valid_init_data):
data = safe_parse_webapp_init_data(TOKEN, valid_init_data)
assert data.user.id == 7
def test_safe_parse_tampered_raises(valid_init_data):
bad = valid_init_data.replace("Test", "Hacker") # user'ni buzamiz -> imzo mos kelmaydi
with pytest.raises(ValueError):
safe_parse_webapp_init_data(TOKEN, bad)
Haqiqiy natija (RUN qilingan):
Beshta test ham o'tdi. Diqqat: test_safe_parse_tampered_raisesda biz userdagi ismni o'zgartirdik β imzo darhol "yiqildi". Bu β initDataning butunligini (integrity) ta'minlovchi mexanizm.
24.7 β FastAPI'da autentifikatsiya qatlami¶
Endi hammasini birlashtiramiz: backend'da har bir himoyalangan endpoint avtomatik initDatani tekshiradigan Depends qatlami quramiz. Bu pattern 25-bobdagi to'liq Mini App backend'ining poydevori bo'ladi.
Frontend initDatani Authorization: tma <initData> header'ida yuboradi (Telegram tomonidan tavsiya etilgan sxema). Backend'da dependency uni ajratib, tekshiradi:
# app.py β FastAPI auth dependency
from fastapi import FastAPI, Depends, Header, HTTPException
from aiogram.utils.web_app import safe_parse_webapp_init_data, WebAppInitData
TOKEN = "123456:AAH-Test_abc" # productionda env'dan oling, kodga yozmang!
app = FastAPI()
def auth(authorization: str = Header(default="")) -> WebAppInitData:
if not authorization.startswith("tma "):
raise HTTPException(status_code=401, detail="Authorization header yo'q")
init_data = authorization[4:]
try:
return safe_parse_webapp_init_data(TOKEN, init_data)
except ValueError:
raise HTTPException(status_code=403, detail="initData soxta")
@app.get("/me")
def me(data: WebAppInitData = Depends(auth)):
# MUHIM: user.id'ni body'dan EMAS, tekshirilgan initData'dan olamiz
return {"id": data.user.id, "name": data.user.first_name}
/me endpointi user_idni so'rovning body'sidan olmaydi β uni faqat tekshirilgan initDatadan oladi. Shuning uchun hujumchi idni o'zgartira olmaydi: id imzo ichida, imzoni esa token'siz qayta hisoblab bo'lmaydi.
Buni TestClient bilan OFFLINE tekshiramiz β server ishga tushirmaymiz, internetga chiqmaymiz:
# verify_fastapi.py β TestClient bilan OFFLINE
from fastapi.testclient import TestClient
from app import app, TOKEN
# build(...) β yuqoridagidek imzolovchi yordamchi
client = TestClient(app)
init_data = build(TOKEN, {"id": 99, "first_name": "Oqil"}, int(time.time()))
# 1) to'g'ri initData -> 200
r = client.get("/me", headers={"Authorization": f"tma {init_data}"})
assert r.status_code == 200 and r.json()["id"] == 99
# 2) header yo'q -> 401
assert client.get("/me").status_code == 401
# 3) buzilgan initData -> 403
bad = init_data[:-1] + ("0" if init_data[-1] != "0" else "1")
r = client.get("/me", headers={"Authorization": f"tma {bad}"})
assert r.status_code == 403
Haqiqiy natija (RUN qilingan):
1) to'g'ri -> 200 {'id': 99, 'name': 'Oqil'}
2) headersiz -> 401
3) buzilgan -> 403
FastAPI auth dependency OFFLINE tekshiruvi O'TDI
Uchala holat ham to'g'ri ishladi: to'g'ri imzo bilan 200, header'siz 401, buzilgan initData bilan 403. Bu β Mini App backend'ingizning xavfsizlik darvozasi.
Jonli qism (illustrativ): Yuqoridagi kodning hammasi tekshirildi, lekin REAL
initDatafaqat Telegram client Mini App'ni ochganda (HTTPS hosting orqali) hosil bo'ladi. Uni real qurilmadan olib, jonli backend'ga yuborib ko'rish β token + public HTTPS hosting talab qiladi. Tekshiradigan kod aynan shu (yuqorida ko'rganimiz) β faqat jonliinitDatamanbai farq qiladi.
24.8 β Amaliy maslahatlar va keng tarqalgan xatolar¶
- Token'ni env'dan oling. Token kodga yozilsa, repo'ga tushadi β bu token'ni oshkor qiladi.
os.environ["BOT_TOKEN"]ishlating (13-bob). auth_dateni tekshiring. aiogram util buni qilmaydi β replay himoyasi sizdan. 24 soat (yoki sizning ehtiyojingizga ko'ra qisqaroq) muddat qo'ying.==emas,compare_digest. Imzolarni solishtirishda doimhmac.compare_digestβ timing-hujumdan himoya.user.idni faqat tekshirilganinitDatadan oling. So'rov body'sidagiuser_idga hech qachon ishonmang.- HTTPS shart.
initDataheader'da ochiq uzatiladi; HTTP bo'lsa, uni MITM ushlab,max_ageichida replay qilishi mumkin. Mini App umuman faqat HTTPS'da ochiladi. - Saralashni o'zgartirmang.
data_check_stringaniq saralangan tartibda bo'lishi shart. Qo'lda yozsangizsorted(...)ni unutmang (yoki shunchaki aiogram util'ini ishlating). hashni log'ga yozmang. Imzoni log'da saqlash β kerakmas oshkoralik. Userid/usernameni log qilish kifoya.
Mashqlar¶
Oson¶
data_check_stringnima va u nima uchun kalit bo'yicha saralanadi? Saralash buzilsa nima bo'ladi?- Bot token'i nima uchun
initDataxavfsizligining markazida turadi? Agar hujumchi token'ni bilsa, u soxtainitDataqura oladimi? hmac.compare_digestoddiy==dan nimasi bilan farq qiladi va nega bu farq xavfsizlik uchun muhim?check_webapp_signaturevasafe_parse_webapp_init_dataorasidagi farqni ayting: qaysi biri qachon ishlatiladi?- Frontend'dagi
if (user.is_admin)tekshiruvi nima uchun himoya emas? Hujumchi uni qanday chetlab o'tadi? secret_keyni hisoblashda HMAC'ning kaliti nima, xabari nima? (b"WebAppData"va token).
O'rta¶
validate_init_datafunksiyasini yozing (24.3'dagidek), so'ng to'g'riinitDataqurib uniassertbilan tasdiqlang. Internetsiz ishga tushiring.auth_dateni 25 soat oldingi qilibinitDataquring vamax_age=86400bilan rad etilishini tekshiring.initDataningusermaydonidagiidni o'zgartiring (imzoni qaytadan hisoblamasdan) vacheck_webapp_signatureFalseqaytarishini ko'rsating.pytestbilan kamida 3 ta test yozing: to'g'ri imzo (True), buzilgan (False), noto'g'ri token (False).- FastAPI
Depends(auth)endpointini quring,TestClientbilan200/401/403holatlarini sinang. safe_parse_webapp_init_dataqaytargan obyektdanuser.id,user.usernamevaauth_dateni chiqaring.auth_datening turi nima?
Qiyin¶
- Replay-hujumni
max_agedan ham qattiqroq to'sing: tekshirilgan har bir(user_id, auth_date)juftligini qisqa muddatga (masalansetyoki Redis'da) saqlab, takroran kelgan bir xilinitDatani rad eting. Buni OFFLINE simulyatsiya qiling. aiogram.utils.web_appmanbasini o'qing vacheck_webapp_signatureICHKI ishini o'zingiz yozganvalidate_init_databilan solishtiring: ikkalasi bir xil natijani berishini bir xilinitDatadaassertbilan isbotlang.- FastAPI o'rniga aiohttp (13-bobdagi webhook serveringizga mos) bilan auth-middleware yozing:
initDatani header'dan oling, tekshiring,request["user"]ga qo'ying.aiohttp.test_utilsbilan OFFLINE sinang.
Yechimlar
1. data_check_string β hashdan tashqari barcha maydonlarning key=value ko'rinishida, kalit bo'yicha saralangan holda \n bilan ulangan satri. Telegram aynan saralangan tartibda imzolaydi; agar siz boshqa tartibda tuzsangiz, hosil bo'lgan hash Telegram'nikiga mos kelmaydi va to'g'ri initData ham rad etiladi. Demak saralash β algoritmning majburiy qismi.
2. Token β HMAC kalitining manbai (secret_key = HMAC(b"WebAppData", token)). Faqat token egasi (siz va Telegram) to'g'ri hash hisoblay oladi. Ha β agar hujumchi token'ni bilsa, u istalgan initDatani o'zi imzolab, soxta user yubora oladi. Shuning uchun token'ni sir saqlash β butun tizimning poydevori.
3. Oddiy == satrlarni belgi-belgi solishtiradi va birinchi farqda darhol to'xtaydi β solishtirish vaqti mosligiga bog'liq bo'ladi. Hujumchi javob vaqtini o'lchab, hashni belgi-belgi "taxminlab" topishi mumkin (timing attack). hmac.compare_digest esa doim bir xil vaqtda ishlaydi, shuning uchun vaqtdan ma'lumot "oqib chiqmaydi".
4. check_webapp_signature faqat bool qaytaradi β imzo to'g'ri/noto'g'riligini bilish kifoya bo'lganda. safe_parse_webapp_init_data imzoni tekshiradi VA WebAppInitData obyektini qaytaradi (soxta bo'lsa ValueError) β userni ham ishlatish kerak bo'lganda (deyarli har doim). Productionda odatda safe_parse_... ishlatamiz.
5. Frontend brauzerda ishlaydi β foydalanuvchi (yoki hujumchi) uning to'liq nazoratchisi. if (user.is_admin)ni brauzer konsolida user.is_admin = true deb o'zgartirish yoki backend so'rovini curl bilan to'g'ridan-to'g'ri yuborish bir soniyalik ish. Himoya faqat serverda, tekshirilgan initData asosida bo'lishi mumkin.
6. secret_key = hmac.new(key=b"WebAppData", msg=token, ...) β bu yerda kalit b"WebAppData" (doimiy literal), xabar esa bot token'i. (Keyingi bosqichda esa kalit secret_key, xabar data_check_string bo'ladi β joylar almashadi.)
7.
import hmac, hashlib, json, time
from urllib.parse import urlencode, parse_qsl
TOKEN = "123456:AAH-Test_abc"
def validate(token, init_data, max_age=86400):
p = dict(parse_qsl(init_data, strict_parsing=True))
h = p.pop("hash")
dcs = "\n".join(f"{k}={p[k]}" for k in sorted(p))
sk = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
calc = hmac.new(sk, dcs.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(calc, h):
raise ValueError("soxta")
if time.time() - int(p["auth_date"]) > max_age:
raise ValueError("eskirgan")
return json.loads(p["user"])
def build(token, user, ad):
f = {"user": json.dumps(user, separators=(",", ":")), "auth_date": str(ad)}
dcs = "\n".join(f"{k}={f[k]}" for k in sorted(f))
sk = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
f["hash"] = hmac.new(sk, dcs.encode(), hashlib.sha256).hexdigest()
return urlencode(f)
idata = build(TOKEN, {"id": 5, "first_name": "A"}, int(time.time()))
assert validate(TOKEN, idata)["id"] == 5
print("7-mashq: OK")
8.
old = build(TOKEN, {"id": 5, "first_name": "A"}, int(time.time()) - 90000)
try:
validate(TOKEN, old, max_age=86400)
print("XATO: o'tib ketdi")
except ValueError as e:
print("8-mashq: rad etildi ->", e) # -> eskirgan
9.
from aiogram.utils.web_app import check_webapp_signature
# user ichidagi "id":5 ni "id":1 ga o'zgartiramiz (imzoni qayta hisoblamasdan)
tampered = idata.replace("%22id%22%3A5", "%22id%22%3A1")
assert check_webapp_signature(TOKEN, tampered) is False
print("9-mashq: user buzildi -> False")
10.
import pytest
from aiogram.utils.web_app import check_webapp_signature
def test_ok():
assert check_webapp_signature(TOKEN, build(TOKEN, {"id": 1, "first_name": "X"}, int(time.time())))
def test_tampered():
d = build(TOKEN, {"id": 1, "first_name": "X"}, int(time.time()))
assert check_webapp_signature(TOKEN, d[:-1] + ("0" if d[-1] != "0" else "1")) is False
def test_wrong_token():
d = build(TOKEN, {"id": 1, "first_name": "X"}, int(time.time()))
assert check_webapp_signature("000:WRONG", d) is False
11.
from fastapi import FastAPI, Depends, Header, HTTPException
from fastapi.testclient import TestClient
from aiogram.utils.web_app import safe_parse_webapp_init_data, WebAppInitData
app = FastAPI()
def auth(authorization: str = Header(default="")) -> WebAppInitData:
if not authorization.startswith("tma "):
raise HTTPException(401)
try:
return safe_parse_webapp_init_data(TOKEN, authorization[4:])
except ValueError:
raise HTTPException(403)
@app.get("/me")
def me(d: WebAppInitData = Depends(auth)):
return {"id": d.user.id}
c = TestClient(app)
d = build(TOKEN, {"id": 9, "first_name": "X"}, int(time.time()))
assert c.get("/me", headers={"Authorization": f"tma {d}"}).status_code == 200
assert c.get("/me").status_code == 401
assert c.get("/me", headers={"Authorization": f"tma {d[:-1] + '0'}"}).status_code in (403,)
print("11-mashq: 200/401/403 OK")
12.
from aiogram.utils.web_app import safe_parse_webapp_init_data
data = safe_parse_webapp_init_data(TOKEN, idata)
print(data.user.id, data.user.username, data.auth_date)
print(type(data.auth_date)) # -> <class 'datetime.datetime'>
auth_date β datetime obyekti (aiogram uni Unix-vaqtdan avtomatik o'giradi), user esa WebAppUser.
13. Imzo to'g'ri bo'lsa ham, ko'rilgan hashlarni vaqtinchalik saqlaymiz; takror kelganini rad etamiz:
seen = {} # hash -> birinchi ko'rilgan vaqt (Redis ham bo'lishi mumkin, TTL bilan)
def validate_once(token, init_data, max_age=3600):
p = dict(parse_qsl(init_data, strict_parsing=True))
h = p["hash"]
user = validate(token, init_data, max_age) # 7-mashqdagi imzo+yosh tekshiruvi
now = time.time()
# eski yozuvlarni tozalash
for k in [k for k, t in seen.items() if now - t > max_age]:
del seen[k]
if h in seen:
raise ValueError("replay: bu initData allaqachon ishlatilgan")
seen[h] = now
return user
d = build(TOKEN, {"id": 5, "first_name": "A"}, int(time.time()))
validate_once(TOKEN, d) # 1-marta: OK
try:
validate_once(TOKEN, d) # 2-marta: replay
except ValueError as e:
print("13-mashq: replay rad etildi ->", e)
dict faqat bitta process uchun; ko'p-instansli productionda Redis (TTL bilan) ishlating.
14.
from aiogram.utils.web_app import check_webapp_signature
def my_check(token, init_data):
p = dict(parse_qsl(init_data, strict_parsing=True))
h = p.pop("hash")
dcs = "\n".join(f"{k}={p[k]}" for k in sorted(p))
sk = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
calc = hmac.new(sk, dcs.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calc, h)
d = build(TOKEN, {"id": 3, "first_name": "Z"}, int(time.time()))
assert my_check(TOKEN, d) == check_webapp_signature(TOKEN, d) == True
bad = d[:-1] + ("0" if d[-1] != "0" else "1")
assert my_check(TOKEN, bad) == check_webapp_signature(TOKEN, bad) == False
print("14-mashq: qo'lda va aiogram bir xil natija")
15.
from aiohttp import web
from aiogram.utils.web_app import safe_parse_webapp_init_data
@web.middleware
async def auth_mw(request, handler):
authz = request.headers.get("Authorization", "")
if not authz.startswith("tma "):
return web.json_response({"error": "no auth"}, status=401)
try:
request["user"] = safe_parse_webapp_init_data(TOKEN, authz[4:]).user
except ValueError:
return web.json_response({"error": "soxta"}, status=403)
return await handler(request)
async def me(request):
return web.json_response({"id": request["user"].id})
def make_app():
app = web.Application(middlewares=[auth_mw])
app.router.add_get("/me", me)
return app
# OFFLINE test:
import asyncio
from aiohttp.test_utils import TestClient, TestServer
async def run():
d = build(TOKEN, {"id": 8, "first_name": "Q"}, int(time.time()))
async with TestClient(TestServer(make_app())) as client:
r = await client.get("/me", headers={"Authorization": f"tma {d}"})
assert r.status == 200 and (await r.json())["id"] == 8
assert (await client.get("/me")).status == 401
bad = d[:-1] + ("0" if d[-1] != "0" else "1")
assert (await client.get("/me", headers={"Authorization": f"tma {bad}"})).status == 403
print("15-mashq: aiohttp auth-middleware OK")
asyncio.run(run())
β¬ οΈ Oldingi: 23 β Telegram Web App (Mini App) asoslari Β· π README Β· Keyingi: 25 β Mini App backend β‘οΈ