Summary
The public POST /api/chat endpoint has no authentication, no rate limiting, and no input validation. Every request triggers three billed OpenAI calls on the project's key, so anyone can run up unbounded cost by hitting the endpoint in a loop. Malformed requests also crash with an unhandled 500.
Impact
- Cost / abuse: each request fans out to (1) the
detectQueryTypes router call (gpt-4o-mini), (2) at least one text-embedding-ada-002 embedding, and (3) the streaming gpt-4o-mini completion — all billed to OPENAI_API_KEY. A simple loop against the endpoint can exhaust the budget and rate limits. For a public, nonprofit civic project this is a real exposure.
- Robustness: the body is never validated. If
messages is missing or empty, messages[messages.length - 1] throws, and with no try/catch the handler returns a raw 500 that can leak internal error details.
Steps to reproduce
POST /api/chat with {"messages":[{"role":"user","content":"hi"}]} repeatedly in a loop → unbounded OpenAI usage, nothing throttles it.
POST /api/chat with {} → 500, from the unhandled messages[...] access.
Root cause
app/api/chat/route.tsx:
export async function POST(req: Request) {
const { messages } = await req.json() // not validated
const lastMessage = messages[messages.length - 1] // throws if messages missing/empty
// ...no auth check, no rate limit, no try/catch
}
Suggested fix (reuses the existing stack)
- Rate limit by IP with
@upstash/ratelimit + Upstash Redis — Upstash is already a dependency (via QStash), so the account is in place. Sliding window (e.g. N requests/min per IP); return 429 when exceeded.
- Validate the body with Zod (already a dependency): assert
messages is a non-empty array of { role, content } before use; return 400 on failure.
- Wrap the handler in
try/catch → graceful JSON error, no leaked internals.
- Optional: gate to authenticated users via the existing Pehchan OAuth, or add a per-session cap.
Small, self-contained; no schema or data migration.
Scope
Endpoint hardening only (rate limiting, validation, error handling). The retrieval-context fix is tracked separately in #<your Issue 1 number>.
Happy to open a PR for this.
Summary
The public
POST /api/chatendpoint has no authentication, no rate limiting, and no input validation. Every request triggers three billed OpenAI calls on the project's key, so anyone can run up unbounded cost by hitting the endpoint in a loop. Malformed requests also crash with an unhandled 500.Impact
detectQueryTypesrouter call (gpt-4o-mini), (2) at least onetext-embedding-ada-002embedding, and (3) the streaminggpt-4o-minicompletion — all billed toOPENAI_API_KEY. A simple loop against the endpoint can exhaust the budget and rate limits. For a public, nonprofit civic project this is a real exposure.messagesis missing or empty,messages[messages.length - 1]throws, and with notry/catchthe handler returns a raw 500 that can leak internal error details.Steps to reproduce
POST /api/chatwith{"messages":[{"role":"user","content":"hi"}]}repeatedly in a loop → unbounded OpenAI usage, nothing throttles it.POST /api/chatwith{}→ 500, from the unhandledmessages[...]access.Root cause
app/api/chat/route.tsx:Suggested fix (reuses the existing stack)
@upstash/ratelimit+ Upstash Redis — Upstash is already a dependency (via QStash), so the account is in place. Sliding window (e.g. N requests/min per IP); return429when exceeded.messagesis a non-empty array of{ role, content }before use; return400on failure.try/catch→ graceful JSON error, no leaked internals.Small, self-contained; no schema or data migration.
Scope
Endpoint hardening only (rate limiting, validation, error handling). The retrieval-context fix is tracked separately in #<your Issue 1 number>.
Happy to open a PR for this.