Next.js and React applications introduce a specific set of security risks that differ from traditional server-rendered applications. The boundary between server and client is less obvious, environment variables are easy to leak, and the ecosystem moves fast enough that security implications are often underdocumented. This checklist covers the highest-impact issues for production Next.js deployments — with exact configurations, not just general advice.
1. Server-Side Rendering Data Exposure
The most common and least-understood Next.js security risk: anything you return from getServerSideProps or getStaticProps is serialized into the page's HTML and sent to every client that loads the page. This is how Next.js hydrates the client-side React tree — but it means that if you fetch a user's full database record and return the entire object, every field in that object is visible in the page source, including fields you never render.
The fix is explicit field selection. Never return a database record directly:
// Dangerous — leaks all user fields including password hash, internal IDs, etc.
export async function getServerSideProps({ params }) {
const user = await db.user.findUnique({ where: { id: params.id } });
return { props: { user } };
}
// Correct — return only what the page needs
export async function getServerSideProps({ params }) {
const user = await db.user.findUnique({
where: { id: params.id },
select: { id: true, name: true, avatarUrl: true },
});
return { props: { user } };
}
The same risk applies to API responses you fetch in getServerSideProps and pass through to props. Audit every return { props: { ... } } statement and treat it as public data.
2. API Routes Without Authentication
Next.js API routes under /pages/api/ or the App Router's Route Handlers are publicly accessible HTTP endpoints — not internal functions. A common mistake is treating them as if they are only called from your own frontend. They are not: anyone can send a request to https://yourdomain.com/api/users from curl, a browser, or an automated scanner.
Every API route that accesses data or performs mutations must authenticate the request before doing anything else:
// pages/api/orders.js
import { getSession } from 'next-auth/react';
export default async function handler(req, res) {
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Only reach here if authenticated
const orders = await getOrdersForUser(session.user.id);
return res.status(200).json(orders);
}
Also enforce authorization, not just authentication: verify that the authenticated user has permission to access the specific resource they are requesting. Returning user A's data to authenticated user B is a broken access control vulnerability (OWASP A01).
3. Environment Variables — NEXT_PUBLIC_ Leaks Secrets
Next.js exposes environment variables prefixed with NEXT_PUBLIC_ to the browser bundle at build time. This is by design — it is the mechanism for passing public configuration (API base URLs, analytics IDs, feature flags) to the client. The risk is misuse: any secret placed in a NEXT_PUBLIC_ variable is bundled into your JavaScript files and downloadable by anyone.
The rule is simple: secrets belong in server-only environment variables, never in NEXT_PUBLIC_ variables.
# .env.local
# NEVER — these values will be in your client-side JS bundle
NEXT_PUBLIC_DATABASE_URL=postgres://...
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_OPENAI_API_KEY=sk-...
# CORRECT — only available server-side
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...
OPENAI_API_KEY=sk-...
# Fine — this is a public key, designed to be client-side
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
If you have ever used a secret in a NEXT_PUBLIC_ variable in a deployed application, rotate that secret immediately — it has been exposed in your JavaScript bundle and may be cached in CDN edge nodes, browser caches, and web archive snapshots.
4. Security Headers in next.config.js
Next.js does not set security headers by default. Configuring them requires explicit setup in next.config.js. This is the exact configuration to add to any production Next.js deployment:
// next.config.js
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
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=()',
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // tighten for production
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
].join('; '),
},
];
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
module.exports = nextConfig;
The CSP above uses unsafe-inline and unsafe-eval as a starting point. For a stricter policy — which is the goal — replace unsafe-inline with nonce-based or hash-based script allowlisting. Next.js 13+ has built-in nonce support via middleware for the App Router. Note: defining headers in next.config.js does not guarantee they are served — deployment platform configuration, CDN rewrites, or middleware can override them. Always verify headers on the live deployment.
5. dangerouslySetInnerHTML XSS Risk
dangerouslySetInnerHTML exists exactly where its name suggests — it sets raw HTML, bypassing React's escaping. Using it with any user-controlled or external data without sanitization is a stored or reflected XSS vulnerability.
// Dangerous — if content contains <script>alert(1)</script>, it executes
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// Correct — sanitize with DOMPurify before rendering
import DOMPurify from 'isomorphic-dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
The only safe uses of dangerouslySetInnerHTML are with content you fully control: static strings defined in your codebase, or content that has been sanitized server-side with a library like DOMPurify (client-side) or sanitize-html (server-side). Any other use is a vulnerability. Search your codebase: grep -r "dangerouslySetInnerHTML" --include="*.tsx" --include="*.jsx" . and review every result.
6. Image Domain Allowlist to Prevent SSRF via next/image
The next/image optimization API (/_next/image?url=...&w=...&q=...) accepts a URL parameter and fetches the image server-side. Without domain restrictions, this endpoint can be abused as a server-side request forgery (SSRF) vector — an attacker can use it to make your server fetch arbitrary URLs, potentially reaching internal services on your hosting network.
Restrict allowed image domains in next.config.js:
// next.config.js
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.yourdomain.com',
pathname: '/uploads/**',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
// Deprecated but still valid:
// domains: ['images.yourdomain.com', 'res.cloudinary.com'],
},
};
Using remotePatterns (available since Next.js 12.3) is preferred over domains because it allows path-level restrictions in addition to hostname restrictions. The SSRF risk is low in practice because Next.js validates the domain against the allowlist before fetching, but a misconfigured wildcard hostname (** alone) or an overly broad pattern can defeat the protection.
7. Dependency Security with npm audit
Next.js applications typically have hundreds of transitive dependencies. Run npm audit regularly and integrate it into your CI pipeline:
# Fail CI on high or critical severity vulnerabilities
npm audit --audit-level=high
# Fix automatically where possible
npm audit fix
# Review without auto-fixing (safer for production)
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical" or .value.severity == "high")'
The next package itself has had security advisories — keep it current. Subscribe to GitHub security advisories for vercel/next.js to receive notifications. Tools like Dependabot or Renovate automate dependency update PRs and reduce the maintenance burden.
How Shieldome Checks This
Shieldome scans the running, deployed Next.js application — not the source code — and verifies that your security configuration is actually being served in production. This is the critical distinction: security headers defined in next.config.js can be silently overridden by Vercel edge config, CDN rules, or reverse proxy configuration. The code may be correct while the live application is still vulnerable.
In a Shieldome scan of a Next.js deployment, the following checks are performed:
- Security headers from next.config.js are actually being served — Shieldome verifies all six headers from the configuration above are present in live HTTP responses: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and Content-Security-Policy. Missing headers are reported with exact remediation steps.
- HTTPS enforcement is working — Shieldome tests that HTTP requests are redirected to HTTPS with a 301 permanent redirect, that HSTS is present with a max-age of at least 1 year, and that the SSL certificate is valid and not expiring within 30 days.
- Cookie security flags are set on session cookies — verifies that cookies set by NextAuth.js or your session middleware carry the
HttpOnly,Secure, andSameSiteflags. A session cookie withoutHttpOnlyis readable by JavaScript and vulnerable to theft via XSS. - No sensitive files are accessible — probes for
.env,.env.local,.env.production, and.next/directory exposure. Build artifacts in the.next/directory can expose server-side code and configuration. - Server version is not disclosed — checks whether the
ServerandX-Powered-Byheaders reveal the underlying runtime (Node.js version, framework version). Next.js removesX-Powered-Byby default, but this can be re-enabled accidentally by middleware or hosting configuration.
A Shieldome scan of your Next.js deployment takes under 60 seconds and gives you runtime confirmation — not just code-level assurance — that your security configuration is working as intended.
FAQ
Is Next.js secure by default?
More so than older frameworks, but not sufficiently for production without additional configuration. Next.js removes X-Powered-By by default and React escapes values in JSX by default. However, security headers, HTTPS enforcement, cookie flags, and content security policy all require explicit configuration. "Secure by default" for a full-stack framework would mean all of these are configured out of the box — Next.js is not there yet, though Vercel's deployment platform adds some protections automatically.
Does using Vercel to deploy Next.js handle security for me?
Vercel handles infrastructure-level security (DDoS protection, TLS termination, isolated deployments) but does not automatically add security headers to your application. Headers must still be configured in next.config.js or via Vercel project settings. Vercel does add a few headers by default (X-Vercel-ID, etc.) but not the security-relevant ones like CSP or HSTS. You retain responsibility for application-level security regardless of deployment platform.
What is the safest way to handle authentication in Next.js?
Use NextAuth.js (now Auth.js) for a production-ready authentication layer rather than implementing sessions manually. Configure it with the httpOnly: true, secure: true, and sameSite: 'lax' cookie options. Store sessions server-side (database adapter) rather than in JWTs when possible — server-side sessions can be invalidated immediately on logout or compromise, whereas JWTs are valid until expiry regardless of logout. If you use JWTs, keep them short-lived (15-60 minutes) with refresh token rotation.
How do I implement a strict Content Security Policy in Next.js without breaking the app?
Start in report-only mode using Content-Security-Policy-Report-Only instead of Content-Security-Policy. Set a report-uri directive pointing to a CSP reporting endpoint (the free Report URI service at report-uri.com is a common choice). Run in report-only mode for 1-2 weeks across all user journeys, review the violation reports to identify legitimate sources being blocked, add them to your policy, then switch to enforcement mode. Attempting to write a strict CSP from scratch without report-only iteration almost always breaks functionality.
Should I use the Pages Router or App Router for security?
The App Router (introduced in Next.js 13) has some security advantages: React Server Components run exclusively on the server and never send component code to the client, reducing the attack surface for data exposure. Server Actions provide a type-safe RPC layer with built-in CSRF protection via origin header checking. The App Router also has better support for streaming and partial rendering, which reduces the temptation to over-fetch data in getServerSideProps. For new projects, the App Router is the recommended approach. Security fundamentals (authentication, authorization, header configuration, secret management) apply equally to both.
Are React Server Components safe from XSS?
Server Components output HTML, and React still escapes values rendered in JSX — so standard JSX rendering is XSS-safe in Server Components. The risk remains the same as in client components: using dangerouslySetInnerHTML with unescaped user content. The attack surface is actually slightly smaller in Server Components because they do not execute in the browser — injected scripts cannot access the browser DOM or make requests to third-party domains from the server-rendered HTML in the same way. But XSS in a Server Component's output that reaches a Client Component remains a real risk.
How often should I run a security scan on my Next.js app?
At minimum: once per deployment to verify security headers are in place, and weekly ongoing to catch configuration drift. Configuration drift is common with Next.js deployments — a Vercel redeployment, CDN configuration change, or middleware update can silently modify which headers are served. Automated external scanning catches these regressions before attackers do. Shieldome scans take under 60 seconds and can be triggered via API, making it straightforward to add to a post-deployment CI step.
Configuration in code is only half the battle — verify that your Next.js security settings are actually being served in production. Run a free Shieldome scan to check your security headers, HTTPS setup, and cookie configuration in under 60 seconds.