security(broker): harden telegram bridge for production
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

- Validate JWT signature + expiry in /start (was only decoding, not verifying)
- Constant-time signature comparison in telegram-token.ts (prevent timing attacks)
- Rate limit /tg/token endpoint: 10 requests/hour per IP
- Grammy bot.catch() error handler (prevent unhandled rejections crashing broker)
- Cap WS reconnect attempts at 20 (prevent infinite retry loop)
- Expire stale pendingDMs entries (prevent memory leak)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-09 13:20:59 +01:00
parent 0661e6223a
commit a6af0f2154
3 changed files with 53 additions and 24 deletions

View File

@@ -142,6 +142,10 @@ interface PeerConn {
const connections = new Map<string, PeerConn>();
const connectionsPerMesh = new Map<string, number>();
// Rate limiter for /tg/token endpoint (IP → count, cleared hourly)
const tgTokenRateLimit = new Map<string, number>();
setInterval(() => tgTokenRateLimit.clear(), 60 * 60_000).unref();
// --- URL Watch engine ---
interface WatchEntry {
id: string;
@@ -630,8 +634,16 @@ function handleHttpRequest(req: IncomingMessage, res: ServerResponse): void {
return;
}
// Telegram connect token
// Telegram connect token (rate-limited: 10 requests/hour per IP)
if (req.method === "POST" && req.url === "/tg/token") {
const clientIp = (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() ?? req.socket.remoteAddress ?? "unknown";
const tgRateBucket = `tg-token:${clientIp}`;
const tgRateCount = (tgTokenRateLimit.get(tgRateBucket) ?? 0) + 1;
tgTokenRateLimit.set(tgRateBucket, tgRateCount);
if (tgRateCount > 10) {
writeJson(res, 429, { error: "Rate limit exceeded. Max 10 tokens per hour." });
return;
}
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => chunks.push(c));
req.on("end", () => {