5 phút đọc

Một đêm bị spam tin nhắn, sáng ra bill AI vài trăm đô

Chatbot hay tính năng AI trong app mà không có trần chi phí thì chỉ cần một script gửi tin liên tục cả đêm. Cách đặt trần theo người dùng, theo ngày ngay trong code, và bật giới hạn cứng trên trang của OpenAI, Anthropic, Google.

Xem nhanh trong 1 phút

Chatbot trên trang của bạn trả lời khách bằng API của OpenAI hay Claude. Mỗi tin nhắn tốn một ít tiền, ít tới mức không ai để ý. Rồi một đêm có người viết script gửi tin liên tục, mỗi tin dán thêm vài trang văn bản cho dài. Không cần chiếm key, không cần lỗ hổng gì. Họ chỉ dùng đúng cái ô chat bạn mở sẵn.

OWASP gọi kiểu này là Denial of Wallet, nằm trong mục LLM10:2025 Unbounded Consumption của danh sách rủi ro cho ứng dụng LLM. Mục tiêu không phải làm sập app mà làm cạn ví của bạn.

Linh vật robot vội vặn khóa vòi trên chiếc bình heo đất trong suốt, đồng xu tuôn ra, bong bóng tin nhắn bay tới dồn dập

Tiền chảy ra nhanh cỡ nào

Tính thử với con số giả định: giá 3 đô cho một triệu token đầu vào, mỗi tin nhắn kèm lịch sử chat và văn bản dán vào khoảng 8.000 token. Một script gửi 20.000 tin trong một đêm là 160 triệu token, tức khoảng 480 đô, chưa tính token đầu ra. Giá thật của từng model khác nhau, bạn thay vào là ra con số của mình.

Chỗ nguy hiểm là app vibe code thường gửi cả lịch sử hội thoại lên mỗi lần, nên cuộc chat càng dài thì mỗi tin càng đắt. Người dùng bình thường không chat tới tin thứ 500. Script thì có.

Lớp 1: giới hạn ngay trên trang của nhà cung cấp

Làm việc này trước, mất năm phút, và nó là lưới cuối cùng khi code của bạn có lỗi.

  • OpenAI: vào Project settings, mục Limits, phần Spend, bấm Edit spend limit, nhập hạn mức tháng và bật “Enforce a hard limit”. Chạm trần thì API trả lỗi 429 với mã project_spend_limit_exceeded. Tài liệu của OpenAI nói rõ việc chặn không tức thì, số tiền ghi nhận có thể vượt trần một chút. Đặt thêm spend alert ở mức thấp hơn để có email báo trước.
  • Anthropic (Claude Console): tổ chức và từng workspace có spend limit theo tháng. Tách app thật ra một workspace riêng, đặt trần riêng cho nó, để một app bị spam không ăn hết hạn mức của cả công ty.
  • Google Cloud (Gemini API): budget thông thường chỉ gửi cảnh báo, tài liệu của Google ghi rõ nó không tự chặn chi tiêu. Muốn chặn thật thì dùng spend cap budget cho project nếu dịch vụ của bạn hỗ trợ; khi chạm trần, dịch vụ bị tạm dừng tới lúc bạn tự gỡ.

Mỗi app một project hay workspace, mỗi project một key. Như vậy trần đặt ra mới có nghĩa.

Lớp 2: trần theo người dùng, theo ngày trong code

Trần của nhà cung cấp chặn cả app. Khi nó chạm trần, mọi khách đều mất chatbot, kể cả người dùng thật. Vì vậy cần thêm một lớp nhỏ hơn: mỗi tài khoản (hoặc mỗi IP với khách chưa đăng nhập) chỉ được tiêu tối đa một số tiền mỗi ngày.

Ý tưởng giống thẻ điện thoại trả trước. Trước mỗi lần gọi model, kiểm tra còn bao nhiêu trong thẻ. Sau khi gọi xong, trừ đúng số token đã dùng.

-- Postgres / Supabase
create table ai_usage (
  user_id text not null,
  day date not null default current_date,
  cost_micro bigint not null default 0,   -- đơn vị: phần triệu đô
  primary key (user_id, day)
);
// Next.js route handler, ví dụ với OpenAI SDK
const DAILY_LIMIT_MICRO = 200_000      // 0,2 đô mỗi người mỗi ngày, tự chọn theo app
const PRICE_IN = 3, PRICE_OUT = 15     // micro-đô mỗi token, lấy từ trang giá của model bạn dùng

export async function POST(req: Request) {
  const user = await requireUser(req)
  const { message } = await req.json()
  if (typeof message !== 'string' || message.length > 4000) return new Response('Tin quá dài', { status: 413 })

  const spent = await getSpentToday(user.id)
  if (spent >= DAILY_LIMIT_MICRO) {
    return new Response('Hôm nay bạn đã dùng hết lượt, mai quay lại nhé', { status: 429, headers: { 'Retry-After': '3600' } })
  }

  const history = await lastMessages(user.id, 10)          // chỉ gửi 10 tin gần nhất, không gửi cả lịch sử
  const res = await openai.chat.completions.create({
    model: MODEL,
    max_completion_tokens: 500,                            // chặn câu trả lời dài vô tận
    messages: [...history, { role: 'user', content: message }],
  })

  const u = res.usage!
  await addSpent(user.id, u.prompt_tokens * PRICE_IN + u.completion_tokens * PRICE_OUT)
  return Response.json({ reply: res.choices[0].message.content })
}

addSpent nên là một câu insert ... on conflict (user_id, day) do update set cost_micro = ai_usage.cost_micro + $2 để không bị lệch khi hai request chạy cùng lúc. Với Claude thì số token nằm ở usage.input_tokens và usage.output_tokens.

Ba giới hạn nhỏ trong đoạn code trên đáng tiền hơn bạn nghĩ: độ dài tin nhắn, số tin lịch sử gửi lên, và max_completion_tokens. Chúng chặn chiêu “mỗi tin dán một cuốn sách”.

Lớp 3: cảnh báo khi gần trần

Chạm trần mới biết thì đã muộn một nửa. Thêm hai cảnh báo:

  • Tổng chi của cả app trong ngày vượt 50% hoặc 80% mức bạn chịu được thì nhắn Telegram hay gửi email cho bạn. Một cron 15 phút cộng bảng ai_usage là đủ.
  • Một tài khoản chạm trần ngày ba hôm liên tiếp, hoặc tạo hàng chục tài khoản từ cùng một IP, thì đánh dấu để xem tay.

Kết hợp với rate limit thông thường (ví dụ 10 tin mỗi phút mỗi người) nữa là script spam hết đường chạy cả đêm.

Kiểm tra app của bạn trong vài phút

  1. Mở trang billing của nhà cung cấp AI bạn dùng. Có hard limit chưa, hay chỉ có cảnh báo? Key của app đang nằm trong project hay workspace nào?
  2. Tìm trong code chỗ gọi model (chat.completions.create, messages.create, generateContent). Có max_tokens không? Có cắt bớt lịch sử không? Có kiểm độ dài tin nhắn không?
  3. Đăng xuất, mở ô chat. Khách chưa đăng nhập có gọi được AI không? Nếu có, giới hạn theo IP đang ở đâu?
  4. Thử gửi 30 tin liên tục bằng một vòng lặp curl. Nếu tin thứ 30 vẫn được trả lời bình thường, app chưa có rate limit.

App của bạn đã từng bị một đêm bill AI tăng vọt chưa, và lúc đó bạn phát hiện bằng cách nào? 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…