Next.js App-Layer Security: Beyond RLS and Secrets
Next.js app-layer security is everything that isn't Row Level Security or secrets management: XSS through dangerouslySetInnerHTML, eval on untrusted input, wildcard CORS, unvalidated request bodies, and known-vulnerable dependencies. Each is a small, specific fix — and each is invisible until someone exploits it.
Most Next.js + Supabase security advice stops at two things: get your RLS right and keep your secrets server-side. Those are the two biggest levers, and they belong first. But a locked database and hidden keys still leave a third surface exposed: the code that runs between the request and the response. That's the app layer, and it's where the boring, exploitable bugs live.
This is the pillar guide to that layer. Each section is a real footgun with a real fix, and links to a deeper dive where one exists. If you've already handled RLS and secrets, this is your next checklist.
How do I secure the app layer in a Next.js app?
Work through five recurring failure classes, in roughly this order: render untrusted HTML safely, never eval dynamic input, lock down CORS, validate every request body, and patch known-vulnerable dependencies. None are exotic, and all of them ship in real apps every week — the framework doesn't stop you, and nothing breaks when you get them wrong.
That's what makes the app layer different from RLS and secrets: the compiler and the runtime are perfectly happy with every one of these bugs. No error, no failed build, no warning at deploy. You find out when a scanner flags it, or when someone else does.
1. Cross-site scripting through raw HTML
React escapes everything you interpolate into JSX by default, which is why XSS is rare in idiomatic components. The one escape hatch is dangerouslySetInnerHTML, and the name is the documentation. The moment any user-controlled string reaches it — a bio, a comment, markdown rendered to HTML — you have a stored or reflected XSS vector. Sanitize with a vetted library like DOMPurify before rendering, and avoid passing raw user input through it at all.
2. eval and the Function constructor
eval() runs whatever string you hand it, in your app's context, with your app's privileges. If any part of that string is attacker-influenced, that's remote code execution — the most severe class of bug there is. new Function(...) is the same hole. There's almost always a safe alternative: JSON.parse for data, a lookup table for dynamic dispatch, or a real expression evaluator if you genuinely need one. Treat any eval in a codebase as a bug until proven otherwise.
3. Wildcard CORS
Access-Control-Allow-Origin: * tells every website on the internet that it may read your API's responses. On a public, unauthenticated endpoint that's often fine. On a route that returns data behind cookies or an Authorization header, the danger is the next step: pairing * with credentials, which browsers refuse, and then "fixing" that by echoing the request Origin, which hands your users' data to any page they visit. The fix is an allowlist: reflect only a specific set of trusted origins, and never pair * with credentials.
4. Unvalidated request bodies
request.json() returns any. Whatever an attacker POSTs, your handler receives — extra fields, wrong types, missing keys, deeply nested junk. That opens the door to mass-assignment and injection bugs that TypeScript gives you no protection against, because the types are a compile-time fiction the request never honored. Validate the parsed body against a schema — zod's safeParse is the common choice — and return 400 on failure before the data touches anything else.
5. Known-vulnerable dependencies
Plenty of breaches start in node_modules, not in your code. A transitive dependency pinned below its patched version is a known CVE sitting in your bundle, fully documented and trivially discoverable by anyone who reads your lockfile. Run npm audit, watch advisories for the packages you actually ship, and upgrade past the fixed version rather than muting the warning.
Two more that sit at the edge of the app layer
Two footguns straddle the line between app layer and auth, and both are commonly missed:
- Middleware with no matcher. A
middleware.tswith noconfig.matcherruns on every request — static assets, images, prefetches, everything. That's a performance problem, and if your auth logic is path-dependent, a correctness one too. - Server Actions with no auth check. A
"use server"function compiles to a public POST endpoint anyone can call. The ergonomics make it feel like an internal function; it isn't. Resolve and verify the user at the top of every action that mutates data. - Webhooks that skip signature verification. A payment or event webhook that trusts its body without verifying the signature will process forged events — anyone can POST a fake
checkout.session.completedand trigger your fulfillment logic with no payment.
The app layer produces real CVEs. In March 2025, CVE-2025-29927 revealed that Next.js middleware could be bypassed entirely with a crafted
x-middleware-subrequestheader — skipping the auth checks many apps run there. It was patched in Next.js 15.2.3, with back-ports for 12 through 14. Vercel-hosted apps were shielded automatically, but self-hosted deployments runningnext startwithoutput: standalonehad to upgrade or strip the header at the edge. Middleware is app-layer code, and app-layer code has a real attack surface. Our full breakdown of CVE-2025-29927 has the exact affected/patched versions and the header-stripping mitigation.
A 60-second app-layer self-check
Run these against your repo:
# 1. Any raw HTML injection?
grep -rn "dangerouslySetInnerHTML" app/ components/
# 2. Any eval / Function constructor?
grep -rnE "\beval\s*\(|new Function\s*\(" app/ lib/
# 3. Any wildcard CORS?
grep -rn "Access-Control-Allow-Origin.*\*" app/
# 4. Any dependency below a known fix?
npm audit --production
A hit isn't automatically a vulnerability — a sanitized dangerouslySetInnerHTML or an intentionally public wildcard endpoint can be fine. But every hit deserves a deliberate answer, not a shrug.
If you'd rather not run four greps and read four docs, that's the gap GuardLayer fills: it scans a Next.js + Supabase repo for the risky forms of these app-layer issues (plus RLS and secrets) on every push and posts the findings as a PR comment. That means dangerouslySetInnerHTML it can't prove safe, eval on dynamic input, a wildcard or echoed CORS origin sent with credentials (and * on dev or local servers), request input that reaches a dangerous sink, and dependencies on its curated advisory list pinned below the fixed version. A plain * on a route handler without credentials, a body that is unvalidated but never reaches a sink, and advisories outside that list are not flagged, so keep running npm audit. This pillar's spokes — XSS, eval, CORS, input validation, and dependencies — each map to a rule in that engine, and each has a deeper write-up if you want the full story.
FAQ
What is the "app layer" in a Next.js app? It's the code that runs between the incoming request and the outgoing response: route handlers, Server Actions, middleware, and the components that render the result. It sits above the database (where RLS lives) and separate from environment configuration (where secrets live). Bugs here are logic and input-handling bugs — XSS, injection, CORS, validation — rather than access-control or credential bugs.
Is Next.js secure by default?
Partly. React escapes rendered values by default, which prevents most XSS, and Vercel-hosted deployments got automatic protection against the CVE-2025-29927 middleware bypass. But the framework won't stop you from shipping wildcard CORS, an unvalidated request.json(), an eval, or a vulnerable dependency. "Secure by default" covers the framework's own surface, not the code you write on top of it.
Do I still need app-layer checks if my RLS is correct?
Yes. RLS protects rows in your database; it does nothing about an XSS payload in a comment, a reflected CORS origin leaking an authenticated response, or an RCE via eval. The layers are independent — a perfect RLS setup and a wide-open app layer is still a breach waiting to happen. Start with the full security checklist to see how the layers fit together.
How is app-layer security different from dependency scanning?
Dependency scanning (SCA) checks the third-party code you import against a database of known CVEs. App-layer security checks the code you wrote for insecure patterns. You need both: a clean dependency tree with an eval on user input is still exploitable, and hardened application code that imports a vulnerable package is still exposed.
Scan your Next.js + Supabase app — free
GuardLayer runs 36 checks — exposed keys, missing RLS, over-permissive policies, unprotected Server Actions — with the exact fix inline. Connect GitHub and it checks every push too — free on public repos.
No signup, no card. Raw files aren’t kept; only the report is saved.