5 phút đọc

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.

Linh vật robot giơ tay chắn trước nút tròn lớn trên bảng điều khiển, những bàn tay lạ từ bóng tối vươn tới

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ốt

bot.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ần
await bot.api.setWebhook('https://app-cua-ban.com/api/telegram', {
  secret_token: process.env.TELEGRAM_WEBHOOK_SECRET,
})

// trong route nhận webhook
app.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

  1. 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.

  2. 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.

  3. Gửi thử một request giả tới URL webhook:

    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 mà không có header secret là webhook đang nhận mọi thứ. Phải ra 401 hoặc 403.

  4. Mở code tạo nút, xem callback_data có chứa số tiền, id người dùng, vai trò hay không.


Team bạn đang dùng bot nào để duyệt việc nội bộ, và nút Duyệt đang kiểm người bấm ra sao? Vào cộng đồng Vibe Code & Bảo Mật kể cho mọi người nghe nhé.

Nguồn

Chia sẻ lên Facebook

Bình luận

Đang tải bình luận…