Skip to content

12 — Security Audit & Rate Limiting

Status: 📐 Target design — describes the security posture required once a real backend is wired; also documents what the current mock/frontend-only build already gets right (or must be careful not to regress) as it evolves.

🟢 বাংলা ভূমিকা: এই ডকুমেন্টে থ্রেট মডেল, RLS দিয়ে টেন্যান্ট আইসোলেশন, Upstash-ভিত্তিক রেট লিমিটিং, OWASP Top 10 ম্যাপিং, এবং একটা সিকিউরিটি চেকলিস্ট দেওয়া হয়েছে।


⚠️ The sign-in screens in this build are not security. /login, /signup, /forgot-password and /staff-login check nothing; the session is a localStorage object any visitor can write, and SessionGuard runs in the browser after the page and its seed data have already been sent. They exist so the account journeys can be designed. Every mitigation below still reads as unbuilt — nothing in this document is satisfied by them, and the guard should be deleted when server sessions land rather than kept as a second line of defence.

1. Threat Model Summary

ActorPrimary threatPrimary mitigation
Anonymous diner (/table)Tampering with cart totals client-side; forging QR tokens to access another table's sessionServer-side price recomputation at checkout (never trust client-submitted totals); qrToken validated server-side, not just as a UI gate (05 §2)
Restaurant staff (dashboard/KDS/waiter/rider)Cross-tenant data access (staff of tenant A viewing tenant B's orders)RLS tenant isolation (14 §4) + role-based route guards
Malicious tenant ownerAbusing the feature-flag/plan system to unlock paid features without payingServer-side enforcement of isIncludedInPlan() at the API layer, not just hidden in the UI — the whitelist check documented in 03 §4 must be re-validated on every privileged mutation, not only rendered conditionally client-side
Platform admin (insider risk)Abusing impersonation ("View As Restaurant") for unauthorized accessEvery impersonation session start/end is written to AuditLogEntry (scope: platform) with actor + target tenant — see 03 §6
External attackerPayment webhook spoofing; SQL injection; subdomain/host-header spoofing to hijack tenant resolutionSignature verification on all payment webhooks (08 §1); Prisma parameterized queries (no raw SQL string interpolation); strict Host header allowlisting in proxy.ts against known tenant slugs/verified custom domains only
Bot/scraperMenu scraping, promo-code brute forcing, credential stuffing on staff loginRate limiting (§3), CAPTCHA on repeated failed logins, promo code format designed to resist brute-forcing (sufficiently random suffix, not sequential)

🟢 বাংলা সারাংশ: থ্রেট মডেলে ৬ ধরনের সম্ভাব্য আক্রমণকারী চিহ্নিত করা হয়েছে — অ্যানোনিমাস কাস্টমার (ক্লায়েন্ট-সাইড দাম পরিবর্তনের চেষ্টা), স্টাফ (অন্য টেন্যান্টের ডেটা দেখার চেষ্টা), অসৎ রেস্টুরেন্ট মালিক (পেমেন্ট ছাড়া ফিচার আনলক), প্ল্যাটফর্ম অ্যাডমিন (impersonation অপব্যবহার), বাহ্যিক আক্রমণকারী (webhook স্পুফিং, SQL ইনজেকশন), এবং বট (স্ক্র্যাপিং/ব্রুট-ফোর্স)। প্রতিটির জন্য নির্দিষ্ট প্রতিরোধ ব্যবস্থা উল্লেখ করা হয়েছে।


2. Tenant Isolation Defense-in-Depth

Three independent layers, deliberately redundant so a bug in any single layer doesn't cause a cross-tenant leak:

  1. Application layer — every Prisma query filtered by tenantId derived from the resolved session/host, never from a client-supplied parameter.
  2. Database layer (RLS) — Postgres Row-Level Security policies (see 14 §4, tested in §8) enforce the same constraint even if application code forgets a WHERE tenantId = ... clause.
  3. Routing layer — proxy.ts only ever resolves a tenantId from a validated Host header against known slugs/verified custom domains (see 02); a request can never "ask" for a different tenant's data via a query param or body field that bypasses this resolution.

🟢 বাংলা সারাংশ: টেন্যান্ট আইসোলেশনের জন্য তিনটি স্বাধীন স্তর রাখা হয়েছে — অ্যাপ্লিকেশন লেয়ার (প্রতিটি কোয়েরিতে tenantId ফিল্টার), ডাটাবেজ লেয়ার (RLS পলিসি), এবং রাউটিং লেয়ার (Host header যাচাই করে টেন্যান্ট রিজলভ করা)। তিনটি স্তর ইচ্ছাকৃতভাবে রিডানডেন্ট, যাতে একটাতে ভুল হলেও অন্যগুলো ডেটা লিক আটকায়।


3. Rate Limiting (Upstash Redis)

// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export const checkoutLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "1 m"), prefix: "rl:checkout" });
export const loginLimiter    = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "15 m"), prefix: "rl:login" });
export const webhookLimiter  = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(100, "1 m"), prefix: "rl:webhook" });
export const publicApiLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(60, "1 m"), prefix: "rl:public" });

// usage in a route handler:
export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "unknown";
  const { success, limit, remaining, reset } = await checkoutLimiter.limit(ip);
  if (!success) {
    return Response.json({ error: "Too many requests" }, { status: 429, headers: { "X-RateLimit-Remaining": String(remaining) } });
  }
  // ... proceed with checkout
}
Endpoint classLimitRationale
Checkout / order creation5 / minute / IPPrevent order-spam/inventory-lock abuse
Staff login attempts5 / 15 min / (IP + email)Credential-stuffing/brute-force defense
Payment webhooks100 / minute / source IP allowlistGenerous, since these are trusted gateway IPs, but still bounded against replay floods
Public read APIs (menu fetch, promo page)60 / minute / IPProtects against scraping without impacting real users

🟢 বাংলা সারাংশ: Upstash Redis দিয়ে বিভিন্ন এন্ডপয়েন্টে ভিন্ন ভিন্ন রেট লিমিট বসানো হবে — চেকআউটে প্রতি মিনিটে ৫ বার/IP, লগইনে ১৫ মিনিটে ৫ বার (ব্রুট-ফোর্স প্রতিরোধ), পেমেন্ট webhook-এ প্রতি মিনিটে ১০০ বার, এবং পাবলিক রিড API-তে প্রতি মিনিটে ৬০ বার।


4. OWASP Top 10 (2021) Mapping

#RiskApplicability & Mitigation
A01Broken Access ControlRBAC per staff role (04 §7); server-side re-validation of plan/feature entitlements, never trusting client UI gating alone
A02Cryptographic FailuresTLS everywhere (Vercel default); payment credentials never touch the client; passwords (once staff auth is added) hashed with bcrypt/argon2, never stored/logged in plaintext
A03InjectionPrisma's parameterized query builder exclusively — no raw string-interpolated SQL; strict input validation (zod schemas) on every API route body
A04Insecure DesignThreat modeling done upfront (§1) rather than bolted on; webhook-driven payment confirmation (§ 08 §1) instead of trusting client redirects by design
A05Security MisconfigurationEnvironment-scoped secrets (never committed — 10 §5); restrictive default RLS policies (deny-by-default, explicit allow policies)
A06Vulnerable & Outdated ComponentsAutomated dependency updates (Dependabot/Renovate) + npm audit gated in CI
A07Identification & Authentication FailuresRate-limited login (§3); MFA option for platform-admin accounts specifically, given their impersonation/cross-tenant power
A08Software & Data Integrity FailuresSigned payment webhooks verified before trust (§ 08); CI pipeline gates (lint/type/test) before any deploy
A09Security Logging & Monitoring FailuresAuditLogEntry for all privileged actions (impersonation, plan changes, VIP overrides — 03); webhook dispatch logs (NotificationDispatchLog)
A10Server-Side Request Forgery (SSRF)Any server-side fetch of a user-supplied URL (e.g. custom domain verification, future image-upload-by-URL features) must validate/allowlist target hosts before fetching

🟢 বাংলা সারাংশ: OWASP Top 10-এর প্রতিটি ঝুঁকির জন্য এই প্রজেক্টে প্রযোজ্য প্রশমন ব্যবস্থা ম্যাপ করা হয়েছে — Broken Access Control থেকে শুরু করে SSRF পর্যন্ত। বিশেষভাবে গুরুত্বপূর্ণ: ফিচার এনটাইটেলমেন্ট শুধু UI-তে না লুকিয়ে সার্ভার-সাইডেও যাচাই করা, পেমেন্ট webhook সাইনেচার যাচাই করা, এবং সব প্রিভিলেজড অ্যাকশন অডিট লগে রাখা।


5. Security Checklist (pre-launch gate)

  • All Prisma models have RLS enabled with deny-by-default policies before any table is queried through the public API surface.
  • Every payment webhook handler verifies a cryptographic signature before acting on the payload — no exceptions, no "trust this one internal test path" backdoors left in production code.
  • proxy.ts's Host-header resolution rejects any host not matching a known tenant slug pattern or a customDomain marked verified in the database — no fallback to a "default" tenant on unrecognized hosts.
  • Feature-flag/plan whitelist checks (isIncludedInPlan) are re-validated server-side on every mutating request that touches a gated feature, not only rendered conditionally in the dashboard UI.
  • Impersonation ("View As Restaurant") sessions are time-boxed, clearly bannered in the UI for the duration, and fully audit-logged with start/end timestamps and the acting admin's identity.
  • Secrets audit: no API keys/tokens present in client-bundled code (NEXT_PUBLIC_* prefix reserved strictly for values safe to expose).
  • Rate limiting active on checkout, login, webhook, and public read endpoints before public launch.
  • Dependency vulnerability scan (npm audit / Snyk / Dependabot) clean or triaged before each production deploy.

🟢 বাংলা সারাংশ: লঞ্চের আগে একটা সিকিউরিটি চেকলিস্ট পূরণ করতে হবে — RLS সক্রিয় আছে কিনা, পেমেন্ট webhook সাইনেচার যাচাই হচ্ছে কিনা, অজানা হোস্টের জন্য কোনো ডিফল্ট টেন্যান্টে ফলব্যাক হচ্ছে না তো, ফিচার-হোয়াইটলিস্ট সার্ভার-সাইডে যাচাই হচ্ছে কিনা, impersonation সেশন সময়-সীমাবদ্ধ ও অডিট-লগড কিনা, ক্লায়েন্ট বান্ডেলে কোনো সিক্রেট কী ফাঁস হচ্ছে না তো, রেট লিমিটিং সক্রিয় আছে কিনা, এবং ডিপেন্ডেন্সি ভালনারেবিলিটি স্ক্যান পরিষ্কার আছে কিনা।


6. Response Headers & the CSP Plan

Five security headers ship today from next.config.ts on every response, tenant subdomains and custom domains included: Strict-Transport-Security (two years, subdomains, preload), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: SAMEORIGIN and a Permissions-Policy that leaves only geolocation available (rider tracking, docs/06). X-Powered-By is off.

Content-Security-Policy is deliberately absent, and that is a decision rather than an oversight. Two inline scripts must execute before first paint — theme and accent (CLAUDE.md rule 4) — so a policy tight enough to be worth having needs a per-request nonce:

  1. src/proxy.ts generates a nonce per request (crypto.randomUUID()) and puts it on a request header.
  2. The root and tenant layouts read that header and pass it to both <script nonce> tags.
  3. The policy ships as script-src 'self' 'nonce-…' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; frame-ancestors 'self'; base-uri 'none'; form-action 'self'.
  4. Roll out Content-Security-Policy-Report-Only first, watch the reports for a week, then enforce.

This lands with the backend phase, where proxy.ts is already being changed for custom-domain lookups (docs/14 §7). Shipping unsafe-inline now would be a header that looks like protection and is not — and once it is in place, nobody revisits it.

🟢 বাংলা সারাংশ: আজ পাঁচটা সিকিউরিটি হেডার সব রেসপন্সে যায় — HSTS, nosniff, Referrer-Policy, X-Frame-Options আর Permissions-Policy; X-Powered-By বন্ধ। CSP ইচ্ছাকৃতভাবে এখনো নেই: থিম ও অ্যাকসেন্টের ইনলাইন স্ক্রিপ্ট প্রথম পেইন্টের আগেই চলতে হয়, তাই কাজের CSP-র জন্য প্রতি রিকোয়েস্টে nonce লাগবে — proxy nonce বানাবে, দুই লেআউট সেটা স্ক্রিপ্টে বসাবে, প্রথমে report-only, পরে enforce। ব্যাকএন্ড পর্বে proxy এমনিতেই বদলাচ্ছে, তখনই এটা যোগ হবে। এখন unsafe-inline দিয়ে CSP বসানো মানে এমন একটা হেডার যা দেখতে সুরক্ষা, আসলে না।