Bot Telegram có nút Duyệt: ai cũng bấm được nếu bạn không kiểm
Nút inline trong group không chỉ admin bấm được, callback_data do client gửi lên nên sửa được, webhook không có secret thì ai cũng gọi được. Cách kiểm người bấm, giữ trạng thái ở server, và áp dụng cho bot Discord, Slack, Zalo.
Xem nhanh trong 1 phút
Nhiều người làm bot Telegram cho việc nội bộ tin rằng: bot nằm trong group riêng, chỉ có admin trong đó, nút “Duyệt hoàn tiền” chỉ admin thấy, vậy là đủ an toàn. Cả ba ý đều sai.
Group riêng thì vẫn có người được thêm vào sau, có cộng tác viên, có người đã nghỉ mà chưa bị xóa. Ai trong group cũng thấy nút và bấm được, Telegram không có nút “chỉ admin thấy”. Và quan trọng nhất: dữ liệu của nút (callback_data) do ứng dụng Telegram phía người bấm gửi lên. Tài liệu của thư viện python-telegram-bot nói thẳng là người dùng có thể sửa nó. Bot tin callback_data kiểu refund:order=123:amount=500000 là đang tin chữ do người lạ viết.
Ba lỗi hay gặp
Không kiểm ai bấm. Code AI viết thường chỉ đọc callback_data rồi làm. Mỗi callback có trường from cho biết ai bấm, nhưng không ai đọc.
Để dữ liệu quan trọng trong callback_data. Số tiền, id đơn, vai trò cần cấp nằm thẳng trong nút. Người có thể gửi callback tùy ý sẽ đổi số tiền hay id đơn trước khi gửi. Telegram giới hạn callback_data ở 64 byte, nên nhiều người còn nén dữ liệu lại cho vừa, càng dễ quên kiểm.
Webhook không có secret. URL webhook của bot là một API công khai. Ai biết URL (trong log, trong repo, đoán từ tên miền) đều gửi được một update giả, có from.id là id của admin, và bot tin luôn.
Sửa thế nào
Kiểm người bấm theo danh sách ở server
Ví dụ với grammY cho Node.js:
import { Bot, webhookCallback } from 'grammy'const bot = new Bot(process.env.BOT_TOKEN!)const NGUOI_DUYET = new Set([123456789, 987654321]) // Telegram user id, lấy từ database càng tốtbot.callbackQuery(/^duyet:([A-Za-z0-9_-]{16,})$/, async (ctx) => { if (!NGUOI_DUYET.has(ctx.from.id)) { return ctx.answerCallbackQuery({ text: 'Bạn không có quyền duyệt', show_alert: true }) } // chỉ có id ngẫu nhiên trong nút, mọi thứ khác tra ở server const viec = await db.pendingAction.findUnique({ where: { id: ctx.match[1] } }) if (!viec || viec.status !== 'cho-duyet' || viec.chatId !== ctx.chat?.id) { return ctx.answerCallbackQuery({ text: 'Yêu cầu không hợp lệ hoặc đã xử lý' }) } const { count } = await db.pendingAction.updateMany({ where: { id: viec.id, status: 'cho-duyet' }, data: { status: 'da-duyet', approvedBy: ctx.from.id, approvedAt: new Date() }, }) if (count !== 1) return ctx.answerCallbackQuery({ text: 'Đã có người xử lý' }) await hoanTien(viec.orderId, viec.amount) // số tiền lấy từ database, không từ nút await ctx.editMessageText(`Đã duyệt bởi ${ctx.from.first_name}`) await ctx.answerCallbackQuery()})
Ba ý chính trong đoạn trên: so ctx.from.id với danh sách người được duyệt, callback_data chỉ chứa một id ngẫu nhiên dài trỏ tới bản ghi ở server, và đổi trạng thái trong một câu lệnh có điều kiện để hai người bấm cùng lúc không hoàn tiền hai lần.
Muốn gắn bot với tài khoản trên app của bạn thì cho người dùng bấm “Kết nối Telegram” trong app, app tạo một mã một lần, người dùng gửi /start <mã> cho bot, bot lưu telegram_user_id vào đúng tài khoản. Quyền duyệt sau đó tra theo vai trò của tài khoản trong app, không theo việc họ có mặt trong group.
Đặt secret cho webhook
Telegram cho bạn đặt secret_token khi gọi setWebhook. Mọi request Telegram gửi tới sẽ có header X-Telegram-Bot-Api-Secret-Token với đúng giá trị đó. Request thiếu hoặc sai header thì bỏ. Secret dài 1 tới 256 ký tự, chỉ gồm chữ, số, _ và -, nên sinh bằng openssl rand -hex 32 là hợp.
// đặt một lầnawait bot.api.setWebhook('https://app-cua-ban.com/api/telegram', { secret_token: process.env.TELEGRAM_WEBHOOK_SECRET,})// trong route nhận webhookapp.post('/api/telegram', webhookCallback(bot, 'express', { secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,}))
Bot token cũng là chìa khóa: ai có token là điều khiển được bot, đọc được tin nhắn gửi tới bot. Đừng để token trong code frontend hay repo công khai, và nếu lỡ lộ thì vào @BotFather dùng /revoke để cấp token mới.
Bot Discord, Slack, Zalo cũng vậy
Discord: interaction gửi tới endpoint của bạn có chữ ký Ed25519 trong header X-Signature-Ed25519, Discord bắt bạn kiểm chữ ký này. Sau đó vẫn phải kiểm member.user.id hoặc vai trò của người bấm, vì ai trong server thấy tin nhắn cũng bấm được nút.
Slack: mỗi request có header X-Slack-Signature tính từ signing secret, kiểm nó trước khi xử lý. Payload của nút có user.id, so với danh sách người được duyệt.
Zalo OA và các nền tảng khác: nguyên tắc như nhau. Kiểm request có đúng từ nền tảng gửi tới không bằng cơ chế ký hay secret mà nền tảng cung cấp, rồi kiểm người bấm, và không tin dữ liệu gắn trong nút.
Kiểm tra bot của bạn
Thêm một tài khoản Telegram phụ vào group có bot, bấm nút Duyệt bằng tài khoản đó. Việc được duyệt là bot không kiểm người bấm.
Bấm cùng một nút hai lần thật nhanh, hoặc bấm ở hai máy. Việc chạy hai lần là thiếu bước đổi trạng thái có điều kiện.
botphân quyềnTelegram/anh/web/bot-telegram-nut-duyet-ai-cung-bam-480.webpbot · phân quyền · Telegram
Bot Telegram co nut Duyet: ai cung bam duoc neu ban khong kiem Nut inline trong group khong chi admin bam duoc, callback_data do client gui len nen sua duoc, webhook khong co secret thi ai cung goi duoc. Cach kiem nguoi bam, giu trang thai o server, va ap dung cho bot Discord, Slack, Zalo. bot phan quyen Telegram Nhieu nguoi lam bot Telegram cho viec noi bo tin rang: bot nam trong group rieng, chi co admin trong do, nut "Duyet hoan tien" chi admin thay, vay la du an toan. Ca ba y deu sai.
Group rieng thi van co nguoi duoc them vao sau, co cong tac vien, co nguoi da nghi ma chua bi xoa. Ai trong group cung thay nut va bam duoc, Telegram khong co nut "chi admin thay". Va quan trong nhat: du lieu cua nut (`callback_data`) do ung dung Telegram phia nguoi bam gui len. Tai lieu cua thu vien python-telegram-bot noi thang la nguoi dung co the sua no. Bot tin `callback_data` kieu `refund:order=123:amount=500000` la dang tin chu do nguoi la viet.

## Ba loi hay gap
**Khong kiem ai bam.** Code AI viet thuong chi doc `callback_data` roi lam. Moi callback co truong `from` cho biet ai bam, nhung khong ai doc.
**De du lieu quan trong trong `callback_data`.** So tien, id don, vai tro can cap nam thang trong nut. Nguoi co the gui callback tuy y se doi so tien hay id don truoc khi gui. Telegram gioi han `callback_data` o 64 byte, nen nhieu nguoi con nen du lieu lai cho vua, cang de quen kiem.
**Webhook khong co secret.** URL webhook cua bot la mot API cong khai. Ai biet URL (trong log, trong repo, doan tu ten mien) deu gui duoc mot update gia, co `from.id` la id cua admin, va bot tin luon.
## Sua the nao
### Kiem nguoi bam theo danh sach o server
Vi du voi grammY cho Node.js:
```ts
import { Bot, webhookCallback } from 'grammy'
const bot = new Bot(process.env.BOT_TOKEN!)
const NGUOI_DUYET = new Set([123456789, 987654321]) // Telegram user id, lay tu database cang tot
bot.callbackQuery(/^duyet:([A-Za-z0-9_-]{16,})$/, async (ctx) => {
if (!NGUOI_DUYET.has(ctx.from.id)) {
return ctx.answerCallbackQuery({ text: 'Ban khong co quyen duyet', show_alert: true })
}
// chi co id ngau nhien trong nut, moi thu khac tra o server
const viec = await db.pendingAction.findUnique({ where: { id: ctx.match[1] } })
if (!viec || viec.status !== 'cho-duyet' || viec.chatId !== ctx.chat?.id) {
return ctx.answerCallbackQuery({ text: 'Yeu cau khong hop le hoac da xu ly' })
}
const { count } = await db.pendingAction.updateMany({
where: { id: viec.id, status: 'cho-duyet' },
data: { status: 'da-duyet', approvedBy: ctx.from.id, approvedAt: new Date() },
})
if (count !== 1) return ctx.answerCallbackQuery({ text: 'Da co nguoi xu ly' })
await hoanTien(viec.orderId, viec.amount) // so tien lay tu database, khong tu nut
await ctx.editMessageText(`Da duyet boi ${ctx.from.first_name}`)
await ctx.answerCallbackQuery()
})
```
Ba y chinh trong doan tren: so `ctx.from.id` voi danh sach nguoi duoc duyet, `callback_data` chi chua mot id ngau nhien dai tro toi ban ghi o server, va doi trang thai trong mot cau lenh co dieu kien de hai nguoi bam cung luc khong hoan tien hai lan.
Muon gan bot voi tai khoan tren app cua ban thi cho nguoi dung bam "Ket noi Telegram" trong app, app tao mot ma mot lan, nguoi dung gui `/start <ma>` cho bot, bot luu `telegram_user_id` vao dung tai khoan. Quyen duyet sau do tra theo vai tro cua tai khoan trong app, khong theo viec ho co mat trong group.
### Dat secret cho webhook
Telegram cho ban dat `secret_token` khi goi `setWebhook`. Moi request Telegram gui toi se co header `X-Telegram-Bot-Api-Secret-Token` voi dung gia tri do. Request thieu hoac sai header thi bo. Secret dai 1 toi 256 ky tu, chi gom chu, so, `_` va `-`, nen sinh bang `openssl rand -hex 32` la hop.
```ts
// dat mot lan
await bot.api.setWebhook('https://app-cua-ban.com/api/telegram', {
secret_token: process.env.TELEGRAM_WEBHOOK_SECRET,
})
// trong route nhan webhook
app.post('/api/telegram', webhookCallback(bot, 'express', {
secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,
}))
```
Bot token cung la chia khoa: ai co token la dieu khien duoc bot, doc duoc tin nhan gui toi bot. Dung de token trong code frontend hay repo cong khai, va neu lo lo thi vao @BotFather dung `/revoke` de cap token moi.
## Bot Discord, Slack, Zalo cung vay
**Discord:** interaction gui toi endpoint cua ban co chu ky Ed25519 trong header `X-Signature-Ed25519`, Discord bat ban kiem chu ky nay. Sau do van phai kiem `member.user.id` hoac vai tro cua nguoi bam, vi ai trong server thay tin nhan cung bam duoc nut.
**Slack:** moi request co header `X-Slack-Signature` tinh tu signing secret, kiem no truoc khi xu ly. Payload cua nut co `user.id`, so voi danh sach nguoi duoc duyet.
**Zalo OA va cac nen tang khac:** nguyen tac nhu nhau. Kiem request co dung tu nen tang gui toi khong bang co che ky hay secret ma nen tang cung cap, roi kiem nguoi bam, va khong tin du lieu gan trong nut.
## Kiem tra bot cua ban
1. Them mot tai khoan Telegram phu vao group co bot, bam nut Duyet bang tai khoan do. Viec duoc duyet la bot khong kiem nguoi bam.
2. Bam cung mot nut hai lan that nhanh, hoac bam o hai may. Viec chay hai lan la thieu buoc doi trang thai co dieu kien.
3. Gui thu mot request gia toi URL webhook:
```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://app-cua-ban.com/api/telegram \
-H 'Content-Type: application/json' -d '{"update_id":1}'
```
Ra `200` ma khong co header secret la webhook dang nhan moi thu. Phai ra `401` hoac `403`.
4. Mo code tao nut, xem `callback_data` co chua so tien, id nguoi dung, vai tro hay khong.
---
Team ban dang dung bot nao de duyet viec noi bo, va nut Duyet dang kiem nguoi bam ra sao? Vao [cong dong Vibe Code & Bao Mat](https://www.facebook.com/groups/1867387324580254) ke cho moi nguoi nghe nhe.
## Nguon
- [Telegram Bot API: setWebhook, CallbackQuery](https://core.telegram.org/bots/api), Telegram
- [Arbitrary callback_data](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data), python-telegram-bot
- [Receiving and Responding to Interactions: security and authorization](https://discord.com/developers/docs/interactions/overview#preparing-for-interactions), Discord
- [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack), Slack