Next.js Production Security Baseline: Headers, Auth, and Safe Content Rendering
Next.js Production Security Baseline: Headers, Auth, and Safe Content Rendering
Many Next.js applications work perfectly during development and then expose avoidable risks in production. Admin pages get indexed, tokens are stored in unsafe places, Markdown is rendered as raw HTML, CORS is opened to every origin, and builds skip type checks when delivery gets urgent.
This guide gives you a practical baseline for content sites, technical blogs, lightweight dashboards, and SaaS control panels. The goal is not to collect security buzzwords. The goal is to define what each layer should protect and how to verify it before release.
1. Security headers are the first browser boundary
Security headers do not replace secure code, but they reduce the blast radius of common browser attacks such as XSS, clickjacking, MIME sniffing, and privacy leakage.
At minimum, a production Next.js site should send headers like these:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Each header has a clear job:
nosniffprevents browsers from executing files with the wrong MIME type.DENYandframe-ancestors 'none'reduce clickjacking risk.Referrer-Policylimits how much URL information is leaked to external sites.Permissions-Policydisables browser capabilities that your site does not need.CSPrestricts script, style, image, frame, and connection sources.
In real projects, the Content Security Policy often needs to support analytics, AdSense, image CDNs, and other third-party scripts. Start with a compatible policy, verify the site, then tighten it in small steps.
2. Admin and auth pages must not be indexed
Search engines should discover public pages such as articles, category pages, the about page, the contact page, privacy policy pages, and high-quality editorial content. They should not index login pages, registration pages, admin dashboards, draft editors, or user-only areas.
Use two layers:
- Add page metadata with
robots: { index: false, follow: false }. - Add response headers such as
X-Robots-Tag: noindex, nofollow, noarchivefor/admin/*,/auth/*,/write, and/my-posts.
Using both is safer because static pages, dynamic functions, caches, and crawlers do not always behave the same way. The response header is closer to the network layer, while page metadata expresses the page intent clearly.
3. Do not rely only on localStorage for authentication
Many front-end projects store JWTs in localStorage and attach them with an Authorization: Bearer header. This is convenient, but it has a serious drawback: if an XSS bug exists, attacker-controlled JavaScript can read the token.
A stronger long-term pattern is an HttpOnly cookie:
Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800
The important attributes are:
HttpOnly: client-side scripts cannot read the cookie.Secure: the cookie is only sent over HTTPS.SameSite=Lax: cross-site request abuse is reduced while normal navigation still works.Max-Age: session lifetime is explicit.
If your application already uses localStorage, do not break all users at once. Let the login endpoint set an HttpOnly cookie while preserving backward compatibility, then migrate API authentication gradually.
4. Passwords need slow, salted hashes
Plain SHA-256 is fast and useful for file integrity checks, but it is not appropriate for password storage. Password hashing should use a salt and a deliberately expensive algorithm such as PBKDF2, bcrypt, scrypt, or Argon2.
On Cloudflare Workers, PBKDF2 can be implemented with Web Crypto:
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
)
const bits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
hash: 'SHA-256',
salt,
iterations: 210000,
},
key,
256
)
If old users already have legacy password hashes, keep a compatibility path. Verify the old hash, then upgrade the stored format after a successful login.
5. Markdown rendering needs strict sanitation
Technical blogs often support Markdown. If content only comes from trusted maintainers, the risk is lower. If the system supports user submissions, admin editing, database-backed articles, or AI-assisted drafts, it must treat Markdown as untrusted input.
Main risks include:
- Raw HTML embedded inside Markdown.
- Links using
javascript:,vbscript:, or unsafedata:URLs. - Rendering output directly with
dangerouslySetInnerHTML.
Practical rules:
- Escape raw HTML by default.
- Validate link and image protocols after rendering.
- Add
rel="noopener noreferrer nofollow"to external links. - Escape
<,>,&,\u2028, and\u2029before writing JSON-LD. - Use CSP as a final containment layer.
This keeps the writing workflow flexible while making the content system safer as it grows.
6. CORS should not be open for private APIs
Examples often show:
Access-Control-Allow-Origin: *
That is acceptable for public read-only APIs. It is not appropriate for login, registration, email verification, publishing, admin, billing, or user profile endpoints.
Use an allowlist:
const allowedOrigins = new Set([
'https://example.com',
'http://localhost:3000',
])
if (origin && allowedOrigins.has(origin)) {
headers['Access-Control-Allow-Origin'] = origin
headers['Vary'] = 'Origin'
}
This supports local development while preventing random websites from calling sensitive browser-facing endpoints.
7. CI should fail on serious issues
Skipping TypeScript and lint checks during production builds may save minutes today and cost hours later. A healthy baseline is:
npm ci
npm audit --audit-level=high
npm run lint
npm run typecheck
npm run build
If a dependency has a moderate issue that cannot be fixed safely, document the reason and track the upgrade. Do not hide every audit result just to get a green deployment.
Production checklist
Before releasing a Next.js site, verify:
- The homepage returns 200.
- The sitemap contains only public, high-quality pages.
robots.txtblocks/api/,/admin/, and/auth/.- Login and admin pages send
noindex. - CORS rejects untrusted origins for private APIs.
- Markdown content cannot execute raw scripts.
- Type checks pass.
- Production response headers match the intended security policy.
Conclusion
Next.js production security is a set of boundaries, not a single package. Headers protect the browser, auth protects accounts, password hashing protects credentials, Markdown sanitation protects content, CORS protects APIs, and CI protects release quality.
For a content site, these practices also improve SEO and AdSense readiness: crawlers see a cleaner public surface, private pages stay out of the index, and users get a more trustworthy experience.
Comments
Share your thoughts and join the discussion
Comments (0)
Related Articles
Next.js 15 App Router Guide: Server Components, Routing, Data Fetching, and SEO
A practical guide to the Next.js 15 App Router, covering Server Components, Client Components, nested layouts, data fetching, streaming, routing patterns, performance, and SEO metadata.
Cloudflare D1 Backup and Migration Strategy: From Free Plan to Sustainable Production
Learn where Cloudflare D1 fits in a content website, how to design reliable backups, when a MySQL migration makes sense, and how to move without disrupting production users.
Technical SEO for Content Sites: Sitemaps, Robots.txt, and AdSense Readiness
A practical technical SEO guide for content websites, explaining what belongs in a sitemap, how robots.txt should be configured, why low-value pages hurt AdSense approval, and how to build a long-term content quality gate.
Please or to comment