If your Next.js app enforces authentication inside middleware.ts and nothing else, you shipped an app that an attacker could walk straight past with a single HTTP header. That was the reality of CVE-2025-29927, disclosed in March 2025: a critical authorization bypass (CVSS 9.1) that let a request skip Next.js middleware entirely by setting the internal x-middleware-subrequest header. Teams that treated middleware as their only auth gate were exposed until they upgraded. The lesson is bigger than one CVE — the App Router blurs the line between server and client so thoroughly that the most dangerous mistakes are the ones that look like they run on the server but don't, or that run on the server and leak. This guide walks through where Next.js security actually breaks in 2026 and the concrete patterns that hold.
Why can't middleware be your only authorization layer?
Because middleware is edge-run request preprocessing, not an access-control boundary. CVE-2025-29927 exploited the very mechanism Next.js used internally to prevent recursive middleware invocation — a header the framework trusted from itself but never expected from the outside. The patch (Next.js 14.2.25, 15.2.3, and backports) closed that specific hole, but the architectural takeaway stands: authorize as close to the data as possible. Middleware is fine for coarse routing, redirects, and adding headers. It is not where you decide whether this user can read this record.
Do the real check in the Server Component, Route Handler, or Server Action that touches the data:
// app/dashboard/[orgId]/page.tsx
import { auth } from "@/lib/auth";
import { notFound, redirect } from "next/navigation";
export default async function Page({ params }) {
const session = await auth();
if (!session) redirect("/login");
// Object-level check: does THIS user belong to THIS org?
const membership = await db.membership.findFirst({
where: { userId: session.user.id, orgId: params.orgId },
});
if (!membership) notFound();
return <Dashboard orgId={params.orgId} />;
}
This is the same principle as broken-object-level-authorization defense in any API: never infer permission from the route alone.
What data leaks out of Server Components without you noticing?
Anything you pass across the server/client boundary as props. Server Components run only on the server, which tempts developers to fetch a full database row and hand it to a Client Component — including the passwordHash, internal flags, or Stripe customer IDs that were never meant to leave the server. React serializes those props and ships them to the browser, where they sit in the RSC payload for anyone to read.
Select only what the client needs, and treat the boundary as a trust boundary:
// Server Component — shape the data before it crosses to the client
const user = await db.user.findUnique({ where: { id } });
const safeUser = { id: user.id, name: user.name, avatarUrl: user.avatarUrl };
return <ProfileCard user={safeUser} />; // never pass the whole row
How do you keep server secrets out of the client bundle?
Two rules. First, only variables prefixed with NEXT_PUBLIC_ are inlined into the browser bundle — so a DATABASE_URL or STRIPE_SECRET_KEY must never carry that prefix. Second, put a hard guard on modules that must stay server-side using the server-only package, which throws at build time if the module is ever imported into client code:
import "server-only"; // build fails if this file is imported client-side
export async function chargeCustomer(customerId, amount) {
return stripe.charges.create({ customer: customerId, amount });
}
Server Actions deserve the same suspicion: they are publicly reachable POST endpoints, not private functions. Validate every argument (a schema library like Zod is the norm) and re-check authorization inside the action — the fact that a button is only rendered for admins does not stop anyone from invoking the action directly.
Is the built-in Image component a server-side request forgery risk?
It can be, if you configure it carelessly. next/image proxies and optimizes remote images through your server. If remotePatterns (or the older domains) is left wide open, an attacker can point the optimizer at internal metadata endpoints or private services and use your server as an SSRF relay. Pin it to the hostnames you actually serve images from:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com" },
],
},
};
What headers and CSP should every Next.js app set?
A Content-Security-Policy is your defense-in-depth against XSS, and Next.js supports a nonce-based CSP that works with its inline scripts. Set security headers in next.config.js or middleware — Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options: nosniff, and Referrer-Policy. Use a per-request nonce rather than unsafe-inline, which defeats the purpose. Because CSP misconfigurations are behavioral, the reliable way to confirm they actually hold in a running app is a dynamic scan that exercises the deployed routes rather than a static config review alone.
Next.js security checklist
| Area | Do this | Not this |
|---|---|---|
| Authorization | Check at the data layer (page, handler, action) | Rely on middleware alone |
| Server Components | Pass only client-needed fields as props | Pass full DB rows |
| Secrets | server-only guard + no NEXT_PUBLIC_ prefix | Assume "it's a server file" |
| Server Actions | Validate + authorize inside the action | Trust the calling UI |
next/image | Pin remotePatterns to your hosts | Wildcard remote domains |
| Headers | Nonce-based CSP + HSTS | unsafe-inline CSP |
| Dependencies | Continuous SCA on the lockfile | Occasional npm audit |
Don't forget the dependency tree
Next.js apps sit on hundreds of transitive npm packages, and 2025 proved that tree is a live attack surface: the self-propagating Shai-Hulud worm compromised more than 500 packages (CISA alert, September 2025), and a separate phishing-driven takeover pushed malicious versions of chalk and debug. No amount of App Router hardening helps if a build pulls a trojanized dependency. Continuous software composition analysis across the full dependency graph is the counterpart to the framework-level work above.
How Safeguard Helps
Safeguard's dependency scanning resolves the complete transitive npm tree and cross-references it against known malicious-package and typosquat signals, so a compromised indirect dependency surfaces before it reaches a build. Griffin, our AI analysis engine, reviews new package versions for the behavioral tells — surprise install scripts, obfuscated payloads, unexpected network calls — that defined the 2025 npm incidents. Reachability analysis cuts the CVE list down to the ones your Next.js code actually invokes, and auto-fix pull requests land the minimal version bump tested against your lockfile. Framework hardening — moving auth to the data layer, guarding secrets, pinning image hosts — is still your job; Safeguard closes the half of the risk that lives in code you didn't write.
Lock down your Next.js supply chain today — create a free account, read the documentation, or see how Safeguard compares to Snyk.