← Back to blog
SecurityScanaris Team · 7 min read · October 04, 2026

Security checklist for your Next.js app before launch

Shipping a Next.js app with Cursor, v0 or Bolt is fast, but speed hides flaws you do not see until someone finds them for you. This checklist covers what to review before you hit deploy, with the why of each point, how to check it and the code to fix it. The two-minute version is at the end.

How Next.js draws the line between server and client

Almost every security flaw in Next.js is the same flaw: something that should have stayed on the server crossed into the browser. Server Components run on the server and never send their code to the client; as soon as a file carries 'use client', everything it imports ends up in the public bundle. Route Handlers (app/api/*/route.ts) and Server Actions are public URLs even if you never see them. Middleware runs before everything, but it is not a wall. Once that line is clear, the rest of the list is mechanical.

1. Environment variables: mind NEXT_PUBLIC

Why it matters. Anything starting with NEXT_PUBLIC_ is baked into the browser JavaScript at build time. Anyone can read it with the developer tools. Never put a private API key, a service token or your database secret there.

How to check. Open your .env and your Vercel dashboard and read every public-prefixed variable asking yourself: would anything happen if I posted it on Twitter? If the answer is yes, drop the prefix and move it to the server.

How to fix. Variables without the prefix exist only on the server: read them from Server Components, Route Handlers or Server Actions. To make sure a module holding secrets can never be imported from a client component, add import 'server-only' as its first line: if someone imports it from the wrong side, the build fails instead of leaking the key. More detail in the secrets your app leaks in the browser.

2. Route Handlers and Server Actions: always validate and authorise

Why it matters. An API route is not private because it sits in your folder. It is a public URL anyone can call with curl. And a Server Action, even if invoked from a button, is too: Next.js exposes it as a POST endpoint with an identifier. A button that is hidden from a user does not stop someone hitting the endpoint directly.

How to fix. Check the session and the permissions inside every handler and every action, not just in the UI. Validate the request body with a schema (zod or similar) before touching the database. Never accept the user id from the client: take it from the session. And rate-limit anything that costs money or sends email.

// app/actions/update-profile.ts
'use server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'

const Schema = z.object({ name: z.string().min(1).max(80) })

export async function updateProfile(input: unknown) {
  const session = await getSession()
  if (!session) throw new Error('Unauthorized')
  const data = Schema.parse(input)
  // use session.user.id, never an id coming from the client
  await db.user.update({ where: { id: session.user.id }, data })
}

3. Middleware is not your only authentication

Why it matters. It is tempting to protect all of /dashboard from middleware.ts and forget about it. In March 2025 CVE-2025-29927 was published: an internal header (x-middleware-subrequest) allowed skipping the middleware entirely on affected versions, and with it any session check that lived only there. It was fixed in Next.js 15.2.3 and 14.2.25 (with patches for 13 and 12), but the lesson stands.

How to fix. Update Next.js to a patched version and use middleware as a first layer (redirecting anyone without a session), never as the only one. The check that actually protects the data goes in the page, the handler or the action that serves it.

4. Security headers

Why it matters. By default Next.js sends no defensive headers. Without them your app is open to clickjacking, to the browser executing as a script something that is not, and to downgrade to HTTP.

How to fix. In next.config.js:

// next.config.js
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
  { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]

module.exports = {
  poweredByHeader: false,
  productionBrowserSourceMaps: false,
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}

Content-Security-Policy deserves its own treatment: Next.js injects inline scripts to hydrate the page, so a strict CSP needs a nonce generated in the middleware and passed down to the components. Start in Report-Only mode. The detail of every header is in HTTP security headers explained for humans.

5. Secrets crossing into Client Components

Why it matters. It is easy for a private key to end up in a client component by mistake, especially when the AI rewrites files and moves imports around. If it appears in the served JavaScript, it is already compromised.

How to check. After next build, search .next/static for strings like sk_live, service_role, sk-ant or webhook URLs. If they show up, rotate them and move them to the server with import 'server-only' as a safety net.

6. Remote images and fetching user-supplied URLs

Why it matters. next/image optimises images through your server. If images.remotePatterns allows any host, anyone can use your server as an image proxy and burn your quota. Same with an API route that downloads whatever URL the user passes: it can point at internal services of your infrastructure (SSRF).

How to fix. Restrict remotePatterns to the domains you actually use. If a route must fetch external URLs, validate the scheme (https only), reject private IPs and internal hostnames, and set a timeout.

7. Open redirects

Why it matters. The classic redirect(searchParams.next) after login lets anyone build links on your domain that end on a phishing site: yourdomain.com/login?next=https://evil.example.

How to fix. Accept only relative paths that start with / (and not //), or compare against a list of allowed destinations.

8. Files that should not be reachable

Why it matters. Everything in public/ is served as is: a .env copied there by mistake, a database dump or a service-account.json are one URL away. And source maps in production let anyone read your client code with original names.

How to check. Try yourdomain.com/.env, /.git/HEAD and open a JS file from the bundle to see whether it ends in a sourceMappingURL line pointing to a reachable .map. Look at your public/ folder through a stranger's eyes.

9. Session cookies and auth libraries

Why it matters. Auth.js (NextAuth), Clerk or Supabase with its server package configure cookies correctly by default. What fails is what you add: a cookie of your own without HttpOnly, a weak AUTH_SECRET copied from a tutorial, or the session stored in localStorage.

How to fix. Generate the secret with openssl rand -base64 32, store it without a public prefix, and for any cookie of your own use Secure, HttpOnly and SameSite=Lax.

10. Dependencies and the Next.js version

Why it matters. Your app drags along hundreds of packages, and Next.js itself ships security fixes several times a year. The 2025 middleware case hit whoever was not updating.

How to fix. npm audit before every deploy, Dependabot or Renovate on the repository, and a simple rule: update Next.js in the same month a security patch ships.

Deploying on Vercel: three details

  • Environments. Variables are scoped (Production, Preview, Development). Do not give preview deployments production credentials: a preview is a public URL running your latest code.
  • Preview protection. Turn on Deployment Protection so preview URLs require a Vercel login; otherwise anyone with the link sees the branch in progress.
  • Headers. The ones in next.config.js apply on Vercel as they are; if you also use vercel.json, do not duplicate them.

Your 2-minute checklist

  • No NEXT_PUBLIC_ variable holds a secret; modules with keys carry import 'server-only'.
  • Every Route Handler and Server Action checks the session and validates input; the user id comes from the session.
  • Next.js on a patched version; middleware is not the only auth layer.
  • Security headers in next.config.js; poweredByHeader: false; CSP at least in report mode.
  • Bundle free of sk_live, service_role and webhooks; source maps off.
  • remotePatterns restricted; nothing fetches user URLs without validation.
  • Redirects only to relative paths.
  • public/ without sensitive files; .env and .git unreachable.
  • Cookies with Secure, HttpOnly and SameSite; random AUTH_SECRET.
  • npm audit clean; previews protected and free of production secrets.

Check all of this in 30 seconds

Running this list by hand on every deploy is tedious and easy to forget. Scanaris runs read-only checks against your published site: headers, secrets exposed in the JavaScript, reachable sensitive files, source maps, cookies and CORS. Every finding comes with a prompt ready to paste into Cursor or v0 with the exact fix for Next.js. Leave it on monitoring and it warns you when a vibe coding session reopens something. To go deeper on your stack, see the Next.js security page and the general launch checklist.

Frequently asked questions

Are Server Actions secure by default?

They are more convenient than an API route, not more secure. Next.js exposes them as public endpoints; authentication and validation are yours to add inside each one.

Does middleware protect my routes?

It is good for redirects and cheap checks, but it must not be the only barrier. After the 2025 case, the rule is to check the session where the data is served too.

Do I need CSP with Next.js?

Yes, but with a nonce, and starting in report mode. Without a nonce, a strict CSP blocks the framework's own inline scripts and the page renders blank.

How does your site do?

Run Scanaris and check it in 30 seconds, free.

Scan my site

Related checks

Security guides for your stack