HOW TO SECURE VERCEL - SECURITY GUIDE | VIBEEVAL
Hardening guide for Vercel deploys: env discipline, middleware auth, serverless function exposure, and what to verify on every production push.
SCAN YOUR VERCEL DEPLOY NOW
Paste your production URL after applying the guide — we verify the live surface, not the checklist alone.
Vercel Security Context
Vercel handles a lot of the platform layer — TLS, automatic HTTPS, basic DDoS scrubbing, edge networking. Your residual surface lives in: (1) what’s in the deployed bundle (env-var prefixes, accidentally-public secrets), (2) preview deployment URLs that bypass the auth gate on your custom domain, (3) serverless function configuration, and (4) headers / CORS / rewrites you control through vercel.json or next.config.js. The recurring incident shape is “preview URL works without auth, attacker finds it via the deployment list, all the auth-gated features are accessible.”
AI tools (v0, Cursor, Claude Code) generate Next.js apps that deploy to Vercel in one click. The framework defaults are convenient, not strict: Server Actions may ship without auth, NEXT_PUBLIC_ secrets appear after one “make it work in the browser” prompt, and preview URLs inherit production-shaped features without production controls. This guide is the operator checklist for that residual surface. Platform trust questions live on Is Vercel Safe?.
Security Checklist
1. Use the right env-var prefix per scope
Vercel exposes only NEXT_PUBLIC_* (Next.js) / VITE_* (Vite) / PUBLIC_* (Astro) to the browser. Everything else stays server-side. Audit your env vars in Project → Settings → Environment Variables — confirm secrets (Stripe key, OpenAI key, DB URL) do not have a public prefix. The bug ships when an AI tool suggests NEXT_PUBLIC_STRIPE_SECRET_KEY to “make it accessible from the page.”
# Inventory what Vercel will inject
vercel env ls
# Add a secret only to Production
vercel env add STRIPE_SECRET_KEY production
Rule of thumb: if the value can charge money, dump a database, or call a paid API as you, it is never NEXT_PUBLIC_.
2. Set per-environment values
Each env var has Production / Preview / Development scopes. Set distinct values per env — never share the production key with preview. Preview deploys can be reached by anyone with the URL; sharing prod keys with preview is a key leak waiting to happen.
Practical split:
| Variable | Production | Preview | Development |
|---|---|---|---|
| Database | prod cluster | isolated staging DB | local / Docker |
| Stripe | live or restricted | test keys only | test keys |
| OpenAI | capped prod key | low-cap staging key | personal dev key |
| Supabase | prod project | separate project | local |
3. Enable Deployment Protection on previews
Project → Settings → Deployment Protection (Pro plan): require Vercel auth, password, or shareable link to access preview / branch deploys. Without this, every git push creates a public URL — accessible to anyone who guesses or finds the URL — that has the full app, including all the routes you only meant production users to see.
Hashes in preview hostnames are not secrets. They leak via GitHub checks, OG unfurlers, error trackers, and screenshots. Protection is the control; obscurity is not.
4. Configure auth on every protected route
For Next.js App Router: auth() from @/auth at the top of every server action and route handler. For middleware-based gating: define matcher to cover every protected path, not just the obvious ones. Test by hitting /api/admin/foo from an incognito browser; should return 401.
Do not trust “the link is only in the dashboard nav.” Attackers and curious users type URLs.
5. Audit serverless function code
Each route in app/api/ and pages/api/ is internet-reachable. The first three lines of each handler must check the session. Add input validation (Zod) on the body. For long-running operations, set the function’s maxDuration explicitly so a malicious request can’t exhaust your monthly compute budget.
// app/api/projects/route.ts
import { auth } from "@/auth";
import { z } from "zod";
const Body = z.object({ name: z.string().min(1).max(120) });
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) return new Response("Unauthorized", { status: 401 });
const parsed = Body.safeParse(await req.json());
if (!parsed.success) return Response.json(parsed.error, { status: 400 });
// ...
}
6. Enable Vercel Firewall
Project → Settings → Firewall: enable. Configure rate limits per route, IP allowlists for admin paths, and challenge rules for suspicious traffic. The Firewall sits in front of the function — it stops abusive traffic before it consumes function-execution time and your bill.
Start with stricter limits on /api/auth/*, password reset, and any AI inference route. Expand allowlists carefully; a wrong allowlist can lock out legitimate regions.
7. Configure security headers in vercel.json
{
"headers": [{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'" }
]
}]
}
Tune CSP for your real script hosts (analytics, Stripe.js). A CSP that breaks the app will get deleted under pressure — better a strict-but-working policy than none. Verify with the Security Headers Checker.
8. Audit redirects and rewrites
In vercel.json redirects / rewrites: avoid open-redirect patterns like /r?to=... that obey the query parameter. Stick to specific source → specific destination. Open redirects are phishing pivots — see SSRF / open redirect / OAuth.
// Never:
redirect(searchParams.get("next") ?? "/");
// Always allowlist relative paths you own
const next = searchParams.get("next");
if (next && next.startsWith("/") && !next.startsWith("//")) redirect(next);
9. Verify HTTPS on every domain
Vercel handles HTTPS automatically. For custom domains, confirm the cert covers all variants (example.com, www.example.com, any subdomains in use). Test by hitting http://yourdomain.com and confirming the redirect to https://.
Also confirm apex and www both behave as intended so session cookies are not split across hosts by accident.
10. Configure rate limiting per route
Vercel Firewall does platform-level rate limiting; for application-level (per-user, per-action) use a Redis-backed counter (@upstash/ratelimit or equivalent). Tighten on /login, /signup, /reset-password, /api/expensive. The default is no limit, which means credential stuffing and bill-running attacks are free.
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"),
});
11. Review deployment logs
Project → Deployments → [deployment] → Logs: review weekly for: 5xx error spikes, repeated 4xx auth errors, requests to routes you don’t recognize. Vercel logs are short retention by default — stream to your SIEM if compliance requires longer.
Never leave console.log(process.env) in build or runtime code. Build logs are visible to teammates and, on misconfigured previews, to a wider set of eyes.
12. Configure team permissions
Team → Settings → Members: review quarterly. Owner / Member / Developer / Viewer. Anyone with Developer access can deploy and read env vars — keep the list small. Ex-team members lose access on removal but their sessions persist; rotate sensitive secrets after departure.
Prefer SSO on plans that support it so offboarding is an IdP action, not a scavenger hunt across personal GitHub accounts.
13. Enable audit logs
Team → Settings → Audit Log (Pro+): track deployments, env-var changes, member additions, OAuth integrations. Useful for incident response.
After any suspected compromise (Context.ai-style OAuth abuse or stolen laptop), export audit events and rotate env vars that integrations could have read.
14. Audit Edge Functions
Edge Functions run in V8 isolates with a smaller API surface than Node — but the same security rules apply: validate inputs, check auth, never include secrets in URL params (which end up in logs), set CORS to your origin only.
Edge Middleware is especially easy to misconfigure via matcher — protect both pages and APIs.
15. Configure DNS security
For custom domains: enable DNSSEC at your registrar. Add CAA records limiting cert issuance to Let’s Encrypt (or your CA of choice) — prevents an attacker who compromises another CA from issuing a valid cert for your domain.
16. Review third-party integrations
Team → Integrations: every integration has a token. Audit periodically — disconnect ones you stopped using. Each unused integration is a credential that could be compromised independently of your Vercel account.
Least privilege on OAuth scopes. The April 2026 Context.ai lesson applies: platform integrity does not save over-scoped integrations.
17. Configure cache headers carefully
For pages that include user-specific data: Cache-Control: private, no-store to prevent accidental caching at the edge. For public pages: explicit Cache-Control: public, max-age=... is fine. The bug shape is a logged-in page accidentally cached at the edge and served to other users.
// Route handler / page segment config — keep personalized HTML private
export const dynamic = "force-dynamic";
// or set Cache-Control on the Response for user-specific payloads
18. Audit CORS on API routes
For routes called from your own frontend: don’t add CORS headers (same-origin works without). For cross-origin: allowlist the specific domain, never * with credentials. See CORS credentials misconfig.
// Explicit allowlist
const origin = req.headers.get("origin");
const allowed = new Set(["https://app.example.com"]);
if (origin && allowed.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
}
19. Run a security scan
The Vibe Code Scanner catches the AI-specific deploy-side patterns Vercel hosts: source maps in production, exposed .env.example with real values, the preview-URL-bypass mentioned above. The full VibeEval scan adds BOLA and webhook trust.
Middleware and Server Actions (Next.js)
Edge Middleware is only as good as its matcher. A common AI-generated bug: protect /dashboard in UI links but leave /api/admin and Server Actions unprotected.
// middleware.ts — explicit matcher, fail closed
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*", "/admin/:path*"],
};
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
if (!session && !req.nextUrl.pathname.startsWith("/api/public")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
For Server Actions: authenticate inside each action; do not rely on “the form is only on a protected page.” Anyone can POST the action ID.
"use server";
export async function updateProfile(formData: FormData) {
const session = await auth();
if (!session) throw new Error("Unauthorized");
// validate + authorize ownership before write
}
Cron and internal routes
Vercel Cron hits your routes on a schedule. Protect them:
export async function GET(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
// job body
}
Never leave /api/cron/* open because “nobody knows the path.”
Common AI-generated Vercel mistakes
NEXT_PUBLIC_STRIPE_SECRET_KEY/NEXT_PUBLIC_OPENAI_API_KEY.- Production DB URL available on Preview.
- Deployment Protection off; PR previews indexed or shared.
- Middleware matcher missing API routes.
- Open redirect:
redirect(searchParams.get("next"))without allowlist. Cache-Controlmissing on authenticated HTML → user A sees user B’s cached page at the edge.- Cron/jobs routes publicly callable without a secret header.
- Source maps public on production.
- Server Actions without
auth()because the form sits behind a client redirect. - Supabase
service_rolepassed into a client component “to fix RLS errors.”
ISR, caching, and multi-tenant Next apps
AI scaffolds often enable static optimization without thinking about tenancy:
- Personalized dashboards must not be statically cached at the edge for shared paths.
cookies()/headers()usage should force dynamic rendering where needed.- CDN cache keys must not ignore auth cookies for private content.
When in doubt, force dynamic for authenticated layouts and benchmark later — correct authz beats a free static optimization.
How to verify
vercel env ls
# Confirm secrets are Production-only where required
curl -sI https://your-app.vercel.app | grep -i strict-transport
# HSTS present
curl -sI https://your-preview-url.vercel.app
# Expect auth challenge if Deployment Protection is on
Manual: open preview in incognito without Vercel login — should not show private product data. Grep the production JS for sk_live / service_role. Run Token Leak Checker and Security Headers Checker. Two-user BOLA on every new object route.
# Fail CI if public prefix secrets appear in the build output
grep -rE 'sk_live_|sk_test_|service_role' .next/static || true
Release gate for vibe-coded Next apps on Vercel
- Env matrix reviewed (no prod secrets on Preview).
- Deployment Protection on for non-public previews.
- Middleware + Server Actions authenticated.
- Headers and CORS set deliberately.
- Firewall / rate limits on auth and AI routes.
- Integrations least-privilege audited.
- Live scan on production domain + one sample preview.
- Critical findings closed before announcing the launch.
Related Resources
Free Self-Audit Suite
Five free scanners.
Vibe Coding Security Risk Guide
Full risk catalogue.
Solo Founder Pre-Launch Checklist
12 checks before launch.
Is Vercel Safe?
Platform assessment, Preview risk, and env scoping.
Security Headers Checker
Paste-ready vercel.json header grades.
Vercel Security Checklist
Interactive launch blockers.
Token Leak Checker
Catch NEXT_PUBLIC_ mistakes in the shipped bundle.
next.config headers and middleware order
Security headers in next.config.js cover most document responses; API routes may need explicit headers too. Middleware runs before routes — use it for coarse auth redirects, then re-check sessions inside handlers.
// next.config.js sketch
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Frame-Options", value: "DENY" },
];
module.exports = {
async headers() {
return [{ source: "/:path*", headers: securityHeaders }];
},
};
Server Actions trust boundaries
Treat Server Action arguments as attacker-controlled. Re-load authorization from the session and database; never trust a client-passed userId or role. Validate with zod (or similar) at the boundary.
Environment matrix
| Variable | Production | Preview |
|---|---|---|
| Stripe secret | Live | Test only |
| Supabase service_role | Server only | Separate project |
| Database | Prod | Branch/isolated |
| OAuth redirect | Prod domain | Preview domain allowlist |
Document this matrix where agents can read it (repo SECURITY.md) so “make preview work” does not copy live keys.
v0 + Cursor → Vercel: the usual pipeline
Many teams generate UI in v0 or full apps in Cursor, then deploy to Vercel in one click. Security work that falls between tools:
- Server Actions generated without
auth()— forms look protected by UI routes only. NEXT_PUBLIC_secrets introduced when the model “fixes” client fetch errors.- Preview inherits production Supabase/Stripe because env was set once as All environments.
- Middleware matcher covers
/dashboardbut not/apior action endpoints. - Source maps left on for “easier debug” in production builds.
After every generative session that touches routes or env, re-run the verify commands above and a live scan on both production and one preview URL. See Is Vercel Safe? for platform-level residual risk.
Supabase on Vercel specifically
Lovable exports and Cursor apps often keep talking to Supabase from the browser while hosting on Vercel:
- Never put
SUPABASE_SERVICE_ROLEin Vercel env without confirming it is notNEXT_PUBLIC_. - Use a separate Supabase project for Preview deployments.
- Auth redirect allowlist must include
*.vercel.apppreviews or disable OAuth on previews — do not use*wildcards in production Site URL. - RLS remains the data plane control; Vercel middleware cannot replace it for PostgREST calls from the browser.
Webhooks on Vercel serverless
Stripe/GitHub webhooks need the raw body for signature verification. Framework helpers that parse JSON first break constructEvent. Pattern:
export const runtime = "nodejs";
// disable body parser equivalents; read req.text() / raw buffer
const raw = await req.text();
const event = stripe.webhooks.constructEvent(
raw,
req.headers.get("stripe-signature")!,
process.env.STRIPE_WEBHOOK_SECRET!,
);
Protect cron routes with CRON_SECRET as shown earlier; never rely on path obscurity.
Team offboarding on Vercel
- Remove member from team.
- Rotate env vars they could read (Developer+).
- Revoke personal tokens and unused integrations.
- Audit deploy list for unexpected production pushes.
- Re-check Deployment Protection still required for private previews.
Image optimization and SSRF-adjacent paths
Next.js image optimization and “fetch URL to preview” features can become SSRF if user-controlled URLs reach the server. Allowlist hosts; block link-local and cloud metadata ranges. See SSRF / open redirect. AI scaffolds love “paste a link, we unfurl it” without egress controls.
vercel.json rewrites vs application auth
Rewrites that proxy /api/* to an internal service must not strip auth headers and must not become open proxies. Prefer fixed upstream hosts. Review every AI-generated rewrite block the same day it lands.
Automate Your Security Checks
VibeEval scans your Vercel deployment against every category above plus 305 more probes. Findings ship as paste-ready prompts for your AI editor.
VERIFY THE GUIDE ON PRODUCTION
Config is necessary; proof is better. Probe the live Vercel app for secrets, open handlers, and auth gaps.
14-day free trial · No credit card · Cancel anytime