A security audit was completed on 2026-01-26. Critical SSRF and XSS vulnerabilities are fixed. The items marked with warnings below are recommendations from that audit that are not yet implemented.
Security architecture overview
Google OAuth via Supabase
Admin sign-in uses Google OAuth brokered by Supabase Auth. Access is restricted to a single email domain (
@nj.sgadi.us) enforced in both the auth callback and server-side route guards.Row Level Security
Supabase Row Level Security (RLS) is enabled on the database. Policies restrict read and write access to the
registrations and seva tables to authenticated admin users only.URL allowlisting
The
/api/download route validates every requested URL against an explicit domain allowlist before proxying it. Private IP ranges and HTTP (non-TLS) URLs are blocked.Security headers
Five security headers are applied globally via
next.config.mjs: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, and Permissions-Policy.Authentication and domain restriction
The admin portal at/admin/registrations uses Google OAuth via Supabase. After the OAuth flow completes, the auth callback at /auth/callback checks the signed-in user’s email domain before granting access.
Domain validation is implemented in lib/admin-auth.ts:
lib/admin-auth.ts
ALLOWED_DOMAIN constant in lib/admin-auth.ts and redeploy.
Authentication flow:
- User visits
/admin/registrationsand clicks Sign in with Google. - Supabase Auth redirects to Google’s OAuth consent screen.
- After consent, Google redirects to the Supabase callback URL.
- Supabase sets session cookies and redirects to
/auth/callback. /auth/callbackcallsisAllowedAdminDomain()on the session email.- Authorized users proceed to the dashboard; unauthorized users are sent to
/admin/registrations/unauthorized.
Session management
Session refresh is handled by Next.js middleware inmiddleware.ts, which runs on every request except static files:
middleware.ts
updateSession (from utils/supabase/middleware.ts) reads the Supabase session from cookies and refreshes expired tokens transparently.
Row Level Security
Supabase RLS restricts which database rows a user can read or write at the database layer, independent of application logic. The following tables require RLS policies:| Table | Access requirement |
|---|---|
registrations | Admin users (@nj.sgadi.us) — read only |
spiritual_seva_submission | Admin users — read; public — insert |
community_seva_records | Admin users — read; public — insert |
personal_seva_submission | Admin users — read; public — insert |
URL validation and download security
The/api/download route proxies file downloads to prevent exposing direct CDN URLs in the client. It enforces:
- Domain allowlist — only
cdn.njrajatmahotsav.comandimagedelivery.netare permitted - HTTPS-only — HTTP URLs are rejected
- Private IP blocking — loopback, RFC 1918, and link-local addresses are blocked
- 10 MB file size limit — checked via
Content-Lengthheader and blob size - 10-second timeout — via
AbortSignal.timeout(10000) - Filename sanitization — non-alphanumeric characters replaced with
_
app/api/download/route.ts
Security headers
Five HTTP security headers are applied to every route vianext.config.mjs:
next.config.mjs (headers excerpt)
| Header | Value | Effect |
|---|---|---|
X-Frame-Options | DENY | Prevents the site from being embedded in an <iframe> (clickjacking protection) |
X-Content-Type-Options | nosniff | Stops browsers from MIME-sniffing responses away from the declared content type |
X-XSS-Protection | 1; mode=block | Enables the browser’s built-in XSS filter |
Referrer-Policy | strict-origin-when-cross-origin | Limits referrer information sent to cross-origin requests |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disables camera, microphone, and geolocation access |
Environment variable protection
All secrets are stored in environment variables — no hardcoded credentials exist in the codebase..env.local is listed in .gitignore and is never committed.
Email validation
Registration forms validate email addresses with a Zod schema that goes beyond format checking — it also verifies the top-level domain against the IANA TLD list to catch common typos:lib/email-validation.ts
This validation runs client-side only. Before going to production, duplicate the same Zod schema in the relevant API routes so validation is enforced server-side regardless of what the client sends.
Pre-deployment security checklist
Complete every item below before accepting real user data.1. Configure Supabase RLS policies
1. Configure Supabase RLS policies
Enable and verify Row Level Security on all four tables (
registrations, spiritual_seva_submission, community_seva_records, personal_seva_submission) in the Supabase dashboard.- Confirm admin-only read policies for
registrations - Confirm public-insert / admin-read policies for seva tables
- Test policies by attempting reads/writes with a non-admin session
2. Set admin domain
2. Set admin domain
Update After changing, redeploy and verify that a non-matching Google account is blocked.
ALLOWED_DOMAIN in lib/admin-auth.ts to match your organization’s email domain. The current value is @nj.sgadi.us.3. Add rate limiting
3. Add rate limiting
API routes (Alternatively, use Vercel’s built-in rate limiting available on Pro and Enterprise plans.
/api/download, /api/generate-upload-urls, /api/generate-cs-personal-submision-upload-urls) are currently unauthenticated and have no rate limiting. Add rate limiting before go-live.The Supabase-recommended approach uses Upstash Redis with the @upstash/ratelimit package:4. Enable CSRF protection
4. Enable CSRF protection
No CSRF tokens are currently added to forms or verified in API routes. Before launch:
- Add
SameSite=StrictorSameSite=Laxto session cookies (Supabase handles this, but verify the setting) - Add CSRF tokens to all mutating forms
- Verify tokens in the corresponding API route handlers
5. Add server-side input validation
5. Add server-side input validation
Registration and seva forms currently use Zod schemas for client-side validation only. Duplicate all Zod schemas inside the API route handlers so that validation is enforced at the server regardless of client behavior. Never trust client-supplied data.
6. File upload security
6. File upload security
File upload endpoints currently accept client-controlled content types with no server-side validation. Before launch:
- Validate file magic bytes server-side (not just the
Content-Typeheader) - Enforce a file type allowlist (e.g.
image/jpeg,image/png,application/pdf) - Set strict maximum file sizes
- Randomize uploaded filenames to prevent enumeration
- Consider virus scanning for untrusted uploads
Fixed vulnerabilities
The following issues were identified in the 2026-01-26 security audit and are resolved.| Issue | Severity | Location | Fix |
|---|---|---|---|
| SSRF via download proxy | Critical | app/api/download/route.ts | URL allowlist, HTTPS-only, private IP blocking, size limits |
XSS via dangerouslySetInnerHTML | High | components/organisms/standard-page-header.tsx | Removed; all user content rendered safely |
| PII in console logs | Medium | Registration form handlers | Removed all console.log calls containing PII |
| Missing security headers | Medium | next.config.mjs | Five headers added globally |
