skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
131 lines (101 loc) • 4.54 kB
Markdown
## Contents
- 13. Security Hardening <a name="security"></a>
- Checklist
- Webhook Secret Validation
- Input Sanitization
- Rate Limiting (serverless-safe)
- Never Expose Bot Token
## 13. Security Hardening <a name="security"></a>
### Checklist
- [x] **Validate initData HMAC on every API request** — never trust client-side data
- [x] **Verify webhook secret header** — prevents forged webhook calls
- [x] **Check auth_date freshness** — reject stale initData (24h max)
- [x] **Use timing-safe comparison** — prevents timing attacks on HMAC
- [x] **Sanitize all inputs** — never trust user data in SQL or messages
- [x] **Rate limit with a shared store** — never an in-memory `Map` on serverless (see below)
- [x] **Treat CloudStorage as client-controlled** — entitlements live server-side, never in CloudStorage
- [x] **Biometry is not auth** — server trust comes only from validated initData
- [x] **Log payment events** — audit trail for disputes; reconcile with `getStarTransactions`
### Webhook Secret Validation
Already shown in the webhook route above. The secret is set via `setWebhook` API and sent by Telegram in the `X-Telegram-Bot-Api-Secret-Token` header.
### Input Sanitization
```ts
// src/lib/sanitize.ts
/**
* Validate and sanitize a product ID.
* Only allow alphanumeric + underscores.
*/
export function sanitizeProductId(input: unknown): string | null {
if (typeof input !== "string") return null;
if (input.length > 64) return null;
if (!/^[a-zA-Z0-9_]+$/.test(input)) return null;
return input;
}
/**
* Validate a Telegram user ID.
*/
export function sanitizeUserId(input: unknown): number | null {
const num =
typeof input === "number" ? input : parseInt(String(input), 10);
if (!Number.isInteger(num) || num <= 0 || num > 2 ** 52) return null;
return num;
}
/**
* Sanitize text for display (strip control characters).
*/
export function sanitizeText(input: string, maxLength = 500): string {
return input
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "") // strip control chars
.slice(0, maxLength)
.trim();
}
```
### Rate Limiting (serverless-safe)
> **Do not ship an in-memory `Map` rate limiter to Vercel/Cloudflare/any multi-instance deploy.** Each serverless invocation may run in a fresh isolate, so the counter resets between requests and across regions — attackers fan out and the limit never trips. The `setInterval` cleanup also won't run on serverless. Use a shared store (Upstash Redis, Vercel KV, Cloudflare Durable Object / Rate Limiting binding, or your DB).
**Recommended — Upstash Redis sliding window** (works on Vercel/Edge/Node, free tier available):
```bash
npm install /ratelimit @upstash/redis
```
```ts
// src/lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Reads UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN from env.
const redis = Redis.fromEnv();
const limiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(30, "60 s"), // 30 req / 60s per key
analytics: true,
prefix: "miniapp:rl",
});
/** Returns true if the caller is OVER the limit (i.e. should be blocked). */
export async function isRateLimited(key: string): Promise<boolean> {
const { success } = await limiter.limit(key);
return !success;
}
```
Use it in the auth middleware, keyed by the validated user id (so it survives IP rotation):
```ts
// inside withTelegramAuth, after validation succeeds:
if (await isRateLimited(`u:${result.data.user.id}`)) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
```
**Vercel KV alternative** (same shape — Upstash-compatible):
```ts
import { kv } from "@vercel/kv";
import { Ratelimit } from "@upstash/ratelimit";
const limiter = new Ratelimit({ redis: kv, limiter: Ratelimit.slidingWindow(30, "60 s") });
```
> For a **single long-lived Node process** (a VPS, not serverless) the in-memory `Map` approach is acceptable, but gate it behind an explicit `process.env.RATE_LIMIT_BACKEND === "memory"` flag and keep the cleanup `setInterval` — never make it the default for a Vercel deploy.
### Never Expose Bot Token
```ts
// ❌ WRONG — bot token in client-side code
const BOT_TOKEN = "<your-bot-token>"; // NEVER hardcode or ship to the client
// ✅ CORRECT — only in server-side code / env vars
// .env.local (never committed to git)
// BOT_TOKEN=<your-bot-token-from-botfather>
// In API routes (server-side only):
const BOT_TOKEN = process.env.BOT_TOKEN!;
```
---