⚠ Lỗ hổng trong bài viết này đã được Discord vá.
Giữa năm 2025, Discord bắt đầu triển khai một loại tiền tệ mới mang tên Orbs.
Khác với Nitro hay các gói mua bằng tiền thật, Orbs chủ yếu được kiếm thông qua hệ thống Quests. Người dùng có thể xem quảng cáo, chơi game đối tác hoặc hoàn thành một số nhiệm vụ nhỏ để nhận Orbs, sau đó đổi lấy avatar decoration, profile effect, theme và nhiều vật phẩm cosmetic khác.
Là một người gần như không dùng Nitro (vì nghèo), mình khá thích hệ thống này.
Chỉ cần cày Quest (hoặc dùng script) là vẫn có thể đổi được một vài món cosmetic đẹp mà không phải bỏ tiền ra mua.
Trong số tất cả các avatar decoration Discord từng phát hành, bộ mình thích nhất lại là Sakura.

Đây là nhóm decoration có lịch sử khá đặc biệt.
Ban đầu chúng xuất hiện dưới dạng Nitro seasonal decoration vào mùa xuân năm 2023. Sau đó biến mất cùng chương trình seasonal cosmetics cũ. Theo Discord, mức độ yêu thích dành cho Sakura thậm chí còn lớn đến mức góp phần thúc đẩy việc ra đời của Discord Shop sau này.
Vấn đề là mình đã bỏ lỡ lần mở bán đầu tiên.
Đơn giản vì lúc đó mình không có Nitro.
Đến năm 2024, Discord mở lại một số cosmetic cũ thông qua sự kiện “The Vault”, trong đó có cả Sakura.
Và do thời điểm đó mình đang vướng lịch quân sự ở trường, mình lại tiếp tục bỏ lỡ luôn đợt đó.
Kể từ lúc ấy, Sakura gần như trở thành một trong những món cosmetic mà mình nghĩ là sẽ không bao giờ có cơ hội sở hữu nữa.
Sau khi Discord chuyển sang hệ thống Orbs, mình vẫn thỉnh thoảng vào Shop xem thử với hi vọng một ngày nào đó chúng sẽ quay trở lại.
Nhưng rồi một câu hỏi khá ngớ ngẩn xuất hiện trong đầu:
Nếu Discord từng đưa chúng trở lại từ The Vault, vậy những vật phẩm này thực sự đã bị vô hiệu hóa chưa? Hay chúng chỉ đơn giản là không còn xuất hiện trên Discord Shop?
Lúc đó mình chưa nghĩ tới bug bounty hay bất kỳ vấn đề bảo mật nào.
Mình chỉ muốn biết Discord đã thực sự vô hiệu hóa chúng, hay chỉ đơn giản là giấu chúng khỏi cửa hàng.
Nếu Discord cho phép client tự gửi request redeem vật phẩm bằng Orbs, liệu backend có thật sự kiểm tra vật phẩm đó còn được phép bán hay không?
Hay mọi thứ chỉ đang được kiểm soát bằng giao diện?
Để trả lời câu hỏi đó, mình quyết định xem thử luồng redeem hoạt động như thế nào.
Theo dõi luồng redeem
Cách nhanh nhất để hiểu một tính năng web là để nó chạy bình thường rồi mở tab Network.
Khi redeem một vật phẩm bằng Orbs, Discord gửi request:
POST https://discord.com/api/v9/virtual-currency/skus/{SKU_ID}/redeem
Ở đây có một khái niệm đáng chú ý là SKU.
SKU (Stock Keeping Unit) là mã định danh nội bộ đại diện cho từng vật phẩm trong cửa hàng. Mỗi theme, avatar decoration hay profile effect đều có một SKU riêng.
Request gửi lên khá đơn giản:
{
"checkout_session_id": "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
}
Thứ khiến mình chú ý không phải SKU mà là trường checkout_session_id.
Trong các hệ thống thanh toán thông thường, checkout session được tạo từ phía server và gắn với người dùng cụ thể, sản phẩm cụ thể, mức giá cụ thể, trạng thái giao dịch hợp lệ. Nhưng ở đây giá trị này trông giống một UUID được sinh trực tiếp từ phía client.
Lúc đó mình nghĩ tới hai khả năng.
-
Backend kiểm tra rất chặt nên giá trị client gửi lên không quan trọng.
-
Backend đang tin tưởng client nhiều hơn mức cần thiết.
Cách duy nhất để biết là thử thay đổi dữ liệu và quan sát phản ứng của hệ thống.
Điều gì xảy ra nếu đổi SKU?
Mình lấy request redeem hợp lệ rồi thay SKU_ID bằng ID của một vật phẩm không xuất hiện trong cửa hàng.
Kết quả khá bất ngờ. Request vẫn trả về thành công 200 OK.
Orbs bị trừ đúng số lượng cần thiết và vật phẩm được thêm vào inventory giống hệt như khi mua bình thường. Backend có kiểm tra người dùng có đủ Orbs hay không, nhưng lại không kiểm tra vật phẩm đó có đang được mở bán hay không, và cũng không kiểm tra vật phẩm đó có được phép mua bởi người dùng hiện tại hay không.
Mình cũng không biết nên gọi cái bug này là loại gì 😅
Điều duy nhất mình biết là có một sự khác biệt giữa những gì Discord Shop hiển thị và những gì backend thực sự cho phép.
Proof of Concept
Sau khi request đầu tiên thành công, mình muốn chắc chắn đây không phải một trường hợp đặc biệt.
Có thể mình chỉ vô tình chọn đúng một SKU nào đó còn sót lại trong hệ thống.
Vì vậy mình viết một đoạn script nhỏ để thử nghiệm nhanh hơn với nhiều SKU khác nhau.
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
});
}
async function redeemSKU(skuId, token, superProperties) {
const checkoutSessionId = uuidv4();
const res = await fetch(
`https://discord.com/api/v9/virtual-currency/skus/${skuId}/redeem`,
{
method: "POST",
credentials: "include",
headers: {
authorization: token,
"content-type": "application/json",
"x-discord-locale": "en-US",
"x-discord-timezone": "Asia/Saigon",
"x-super-properties": superProperties
},
body: JSON.stringify({
checkout_session_id: checkoutSessionId
})
}
);
console.log(await res.json());
}
Đoạn script này thực ra không làm gì đặc biệt. Nó chỉ gửi lại đúng request mà Discord client vẫn gửi khi redeem một vật phẩm bằng Orbs. Khác biệt duy nhất là mình được quyền lựa chọn SKU thay vì để Discord Shop quyết định.
Kết quả khá nhất quán. Một số theme cũ vẫn redeem được. Một số avatar decoration đã biến mất khỏi cửa hàng vẫn redeem được. Và điều mình quan tâm nhất là các biến thể Sakura cũng vẫn redeem được.
Tuy nhiên không phải SKU nào cũng hoạt động.
Mình cũng thử với một số cosmetic được đánh dấu là Exclusive to Nitro. Những SKU này đều bị từ chối.
Điều đó cho thấy backend vẫn đang thực hiện một số kiểm tra nhất định, thay vì chấp nhận mọi request một cách mù quáng. Nói cách khác, vấn đề không phải là Discord hoàn toàn không xác thực SKU.
Thứ dường như bị bỏ sót là trạng thái phát hành của một số vật phẩm. Chúng không còn xuất hiện trên Discord Shop, nhưng backend vẫn chấp nhận request redeem nếu biết đúng SKU ID.
Lúc đó mình gần như chắc chắn rằng những vật phẩm này chưa hề bị vô hiệu hóa ở phía backend. Chúng chỉ đơn giản là không còn xuất hiện trên Discord Shop nữa.
Discord đã vá như thế nào?
Một thời gian sau mình thử lại cùng phương pháp thì request không còn hoạt động nữa. Các SKU từng redeem được trước đó bắt đầu bị từ chối.
Mình không biết chính xác Discord đã thay đổi phần nào ở backend. Có thể họ đã bổ sung kiểm tra trạng thái phát hành của SKU. Cũng có thể họ đã thay đổi hoàn toàn cơ chế redeem.
Dù cách triển khai cụ thể là gì, kết quả cuối cùng đều giống nhau: backend hiện không còn chấp nhận việc redeem các vật phẩm đã bị ẩn khỏi cửa hàng.
Kết
Mình không hề có ý định tìm bug hay gì. Mình chỉ muốn biết liệu bộ Sakura mà mình đã bỏ lỡ hai lần có thực sự biến mất hay chưa.
Câu hỏi đó dẫn mình tới tab Network. Tab Network dẫn tới endpoint redeem. Và endpoint redeem cho thấy backend đang chấp nhận nhiều thứ hơn những gì Discord Shop thể hiện.
Một vật phẩm biến mất khỏi màn hình không nhất thiết có nghĩa là nó đã biến mất khỏi hệ thống, và nếu không ai hỏi câu đó thì cũng chẳng ai biết 😁
Đôi khi tất cả bắt đầu chỉ từ một câu hỏi đơn giản:
Nếu mình thay một giá trị khác vào đây thì sao?