fix(02-bot-skeleton-initdata-security): WR-02 fail-closed /telegram/webhook — unarmed WEBHOOK_SECRET rejects (constant 500, category-only log) before grammY's accept-everything compare

This commit is contained in:
araharah-executor
2026-10-22 20:18:34 +08:00
parent 8995498bff
commit 3ea1fdd932
3 changed files with 41 additions and 4 deletions
+19 -3
View File
@@ -6,8 +6,12 @@
* the configured secret token — Telegram's updates must carry
* X-Telegram-Bot-Api-Secret-Token = env.WEBHOOK_SECRET or they are 401'd
* by a constant-time compare BEFORE any bot processing or KV touch
* (T-02-03). The bot is instantiated per request because the token only
* arrives with `env` (research-verified Workers pattern).
* (T-02-03). FAIL-CLOSED (WR-02): if the WEBHOOK_SECRET binding is
* missing/empty at request time, the route rejects outright — grammY's
* compareSecretToken silently accepts EVERY request when its secret is
* undefined, so an unarmed binding must never reach it. The bot is
* instantiated per request because the token only arrives with `env`
* (research-verified Workers pattern).
* POST /api/session → the phase's security gate (T-02-01): the caller
* posts a verbatim Telegram.WebApp.initData string — capped at 4 KB
* (WR-01: over-cap bodies are rejected 413 before any parsing; real
@@ -18,7 +22,8 @@
* this endpoint adds no extra branch and no extra write path (T-02-06).
* Anything else → 404.
*
* D-14: the ONLY log statement is category-only (malformed/tampered/stale) —
* D-14: the ONLY log statements are category-only — malformed/tampered/stale
* on the initData gate, misconfigured on the fail-closed webhook gate —
* never the raw payload, never user ids, never timestamps. Rejection
* responses carry the category only.
* D-17: no secret is ever logged or echoed; the bot token / webhook secret
@@ -34,6 +39,17 @@ export default {
async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/telegram/webhook") {
// WR-02 fail-closed: grammY's compareSecretToken accepts EVERY request
// when its secret is undefined (research-confirmed runtime behavior,
// convenience/webhook.js). A WEBHOOK_SECRET binding left unarmed after
// deploy would therefore silently disable webhook authentication and
// admit unsigned POSTs that inject fabricated updates. This guard runs
// BEFORE createBot/webhookCallback; the rejection is a constant 500
// and the log carries the category only.
if (!env.WEBHOOK_SECRET) {
console.log("[webhook] rejected: misconfigured");
return new Response("Server misconfigured", { status: 500 });
}
const bot = createBot(env); // env-bound token → per-request instantiation
const cb = webhookCallback(bot, "cloudflare-mod", undefined, undefined, env.WEBHOOK_SECRET);
return cb(req);