October 6, 2025
Learn how to assess Next.js apps for SSRF, XSS, CSTI, SSTI, CSRF, cache issues, and data leaks. Practical tips, checks, and tools for bug bounty and pentesting.
Daoud Youssef
Hi, I’m Daoud Youssef, Technical Team Lead at DeepStrike. In this short writeup I explain how to approach testing Next.js applications. I will focus on the concepts and the common attack surface you should check during a blackbox pentest. Let’s jump straight to it.
Next.js is a full-stack React framework that supports server rendering and many server-side features out of the box. The JavaScript ecosystem can be split into libraries and frameworks:
// jQuery (Library)
$('#btn').on('click', () => {
alert('Button clicked!');
});
app.get('/users', (req, res) => {
res.send('All users');
});
According to w3techs, Next.js is used on 2.3% of all websites. This means it is used on more than twenty to twenty five million websites on the internet. This means we, as bug hunters or penetration testers, will face it a lot. Also, as a full stack framework for frontend and backend, developers do not need any other programming language. That makes it spread quickly and will increase quickly in the future, so let’s dive into its components and secrets so we can test it properly when we face it.
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data },
};
}
function Page({ data }) {
return <div>{data.title}</div>;
}
export default Page;
getStaticProps
builds HTML at build time. Good for pages that change rarely. The HTML is generated once during build and served as static content until the next build.revalidate
window. Pages are built at build time and revalidated periodically to rebuild updated static pages. For exampleexport async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts },
revalidate: 60,
};
}
/pages/api/*
or app/api/*
creates server-side endpoints that run on Node.js. Use these for server-only logic, database access, or background tasks without a separate Express server.So, in practice you have two code locations:
getServerSideProps
, middleware, API routes, Server Actions, etc., which execute on the server and are not sent to the client.What the browser receives are HTML responses and client-side JS bundles.
When any bug hunter or penetration tester hears the term render
, the first idea that comes to mind is vulnerabilities related to rendering, for example SSTI or CSTI. So can Next.js be vulnerable to these attacks, or is it immune to them?
For SSTI:
Next.js SSR does not use traditional template engines, so SSTI is not a native risk. However, server side template injection can occur if the developer explicitly uses a template engine, EJS, Handlebars, or Pug, or evaluates untrusted input on the server, for example eval
, Function()
, or rendering with a template engine. As a bug hunter, you should still test for it because this is black box testing and we should test everything. Sometimes this approach can lead to surprising vulnerabilities. You might be sure it is not there, but using some library or microservice inside backend code could produce unpredictable issues, so we should test for everything.
{{...}}
into client supplied fields will not normally be interpreted as template code by Next.js itself. Therefore, SSTI is not an inherent or automatic threat in pure Next.js SSR.import ejs from 'ejs';
export default function handler(req, res) {
const tmpl = "<h1>Hello <%= name %></h1>";
const html = ejs.render(tmpl, { name: req.query.name });
res.send(html);
}
EJS stands for Embedded JavaScript Templates. It is a template engine for Node.js that lets developers generate HTML pages by embedding JavaScript logic directly inside HTML. If a developer uses a template on the backend, it could cause SSTI in the application. Since we do not know if a template engine is used on the backend, we should test with simple payloads like {{7*7}}
, <%=7*7%>
, #{7*7}
, and ${7*7}
and see whether any are rendered to forty nine, cause a template error, or are simply reflected.
For CSTI and XSS:
As mentioned earlier, Next.js is built on React, and by default React escapes values before rendering them into the DOM. This means classic CSTI payloads, {{7*7}}
or {{constructor.constructor('alert(1)')()}}
, will not work in pure Next.js CSR. CSTI in Next.js CSR can happen only if unsafe coding patterns are used, such as the dangerouslySetInnerHTML
function.
If a client side template engine, like Handlebars or a Vue runtime, is bundled in the app without sanitization, this could lead to CSTI and also risks XSS. Always check for dangerouslySetInnerHTML
usage or imported template engines when doing static analysis. If you find any of them, start injecting payloads like {{7*7}}
, {{this}}
, and {{constructor.constructor('alert(1)')()}}
into inputs that reflect client side and observe whether they are rendered safely as plain React, or rendered differently, or produce template errors on the page or in the console.
By default, React escapes dangerous characters, <
, >
, and &
, so if userInput = "<script>alert(1)</script>"
, the browser will show it as text. Sometimes developers want to inject raw HTML directly into the DOM. For that, React provides a special prop.
<div dangerouslySetInnerHTML={{ __html: userInput }} />
Open DevTools and inspect the element after you inject your payload. If you see your payload in the DOM as actual HTML, for example <img src=x onerror=alert(1)>
, then it was injected raw. If it appears escaped, <img src=x...>
, then React is escaping and there is no dangerouslySetInnerHTML
.
Applications that allow rich text editing, blogs, CMS, or comments with formatting, often sanitize input poorly and use dangerouslySetInnerHTML
to render. Features like Markdown rendering, WYSIWYG editors, or CMS imports are common hotspots.
Note: A WYSIWYG editor is a software interface that allows users to create and edit content in a visual format, so the content appears on the screen exactly as it will when published or printed.
To search for the existence of these functions in Burp Suite after crawling or browsing the web application, Next.js files usually appear under paths starting with _next/static
or /static
. You can search for dangerouslySetInnerHTML
, __html
, innerHTML
, insertAdjacentHTML
, and setInnerHTML
. If you find any, follow the code to see whether there is a sanitizer function or whether it is rendered directly, then test for CSTI or XSS.
In older versions of Next.js, even though React escaped dangerous characters, <
, >
, and &
, there were issues with XSS via the javascript:
protocol and the data:
protocol. Now, the javascript:
protocol is blocked by React itself, and the data:
protocol is blocked by most modern browsers.
__NEXT_DATA__
as SSR props leakagegetServerSideProps
or other SSR logic can return sensitive data, API keys, internal URLs, or secrets, inside the page props. You can parse the page source for <script id="__NEXT_DATA__">
and inspect the JSON for secrets, tokens, or internal URLs.
SSR or SSG pages, or user customized pages, may be cached by a CDN or a reverse proxy without appropriate Vary
or Cache Control
settings. Always monitor response cache headers like Vary
and Cache Control
to see whether you could cache another user’s sensitive data, cache deception, and visit it from another account to confirm whether you can see the same data.
Also see whether you can poison the cache with malicious content that will be served to other users, cache poisoning.
In most frameworks, or even in plain JavaScript, developers like to minify code for two reasons. The first is to obfuscate the code and make reversing harder. The second is to reduce the size so it loads and renders faster. When the bundler minifies the original multiple JavaScript files into one or more chunked files, it generates .map
files. These files are used to reverse the minified files.
The link to these files is usually at the end of the JavaScript files as a comment like this.
//# sourceMappingURL=//app.example.com/some/path/file.js.map
If that line exists, the bundle references a map file, but the map file itself may be missing on the server. If the line is missing, the map was likely not generated or was stripped.
You can download this file by opening it directly and copying it into a new text file, or by using a curl command. Then put the JavaScript file and the .map
file in the same directory and use any tool to reverse the minified file to its original state, which could be multiple folders with dozens of JavaScript or TypeScript files. You can also restore minified function and parameter names from single letters, a
, s
, or t
, to their original names, login
, signup
, or roles
, which makes it easier to understand. I like to use this tool.
https://github.com/davidkevork/reverse-sourcemap
For Next.js, production builds do not emit source maps for browser bundles, so you will often find .map
files missing or stripped. This is a build size and security default. If the developer explicitly enabled source map generation in next.config.js
or deployed the map files to the CDN, you might find them. Otherwise you will not find them on the deployed site.
Next.js has a component used to resize images so they can be cached or sent to the user to be rendered in multiple sizes. This function is enabled by default, and Next.js uses a URL like this.
https://domain.com/_next/image?url=/daoud.jpg&w=512&q=150
Next.js makes an internal request to //localhost/daoud.jpg, assuming an image exists at that URL, resizes it with a server side image manipulation library, then returns it to the user. Sometimes developers want to serve images from a CDN or other subdomains, so they adjust the configuration like this.
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.domain.com',
},
{
protocol: 'https',
hostname: '*.domain.com',
},
{
protocol: 'http',
hostname: '*.domain.com',
},
],
}
If the developer intentionally, or by accident, allows loading images from all websites by setting the hostname to *
, this allows an attacker to request an image from any source and have it returned to the user. Even if the developer allows only subdomains, if you can find an open redirect on any subdomain, you can redirect the request to any other internal service because this component follows redirects.
Also, if dangerouslyAllowSVG
is enabled, you can point to a malicious SVG file on your domain, which can lead to XSS.
Another attack vector for SSRF in Next.js is Server Actions.
What are Next.js Server Actions:
Server Actions are async functions that run on the server side but can be invoked from Client Components or Server Components. They are marked by the directive "use server"
either at the top of the function or at the top of a module or file, which tells Next.js and React that this code is server only. They are designed to perform mutations, like updating a database, using the POST method rather than fetching data with GET. So the code on the server side could look like this.
"use server";
import { createPostInDb } from "@/lib/db";
export async function createPost(formData) {
const title = formData.get("title");
const body = formData.get("body");
const newPost = await createPostInDb({ title, body });
return { ok: true, id: newPost.id };
}
And the code on the client side could look like this.
"use client";
import { createPost } from "../actions/postActions";
export default function NewPostClient() {
return (
<form action={createPost} method="post">
<input name="title" placeholder="Title" />
<textarea name="body" placeholder="Body" />
<button type="submit">Create</button>
</form>
);
}
Notice here we invoke the backend function, createPost
, from the formAction
property on the client side between curly brackets, not double quotes. It is not a string as in HTML. It is a JavaScript expression in a JSX file, while an HTML form uses a URL.
Another important note we should consider is that data transferred in both directions between client and server must be safely serialized.
When we talk about serializable data, we mean strings, numbers, booleans, null, plain objects, arrays, and form data. Functions, DOM nodes, symbols, and BigInt cannot be serialized, so they cannot be transferred between client and server.
Next Action header
When a Client Component or <form action={someServerAction}>
triggers a Server Action, Next.js does not always call a plain public URL. Instead, the client sends a POST to an internal Next endpoint and includes a header or form field that identifies the action. That identifier appears as a header, commonly shown as Next-Action
, and maps the incoming request to the specific server function to execute. In practice, the server ignores the request path and uses the Next-Action
token to dispatch the call.
So you can replay that request to trigger the same server action as long as you send the appropriate POST body and the Next-Action
header.
In this writeup, https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps, the Assetnote security team describes the details of CVE-2024-34351, https://nvd.nist.gov/vuln/detail/CVE-2024-34351, which we can summarize in these steps.
Next-Action
header. If it began with a leading slash, the server would fetch that page and render it back to the user. The code snippets showed that the server built the internal request URL from the Host request header, which is controlled by the attacker. If you change the Host to any external host, you get an HTTP interaction, which is blind SSRF.Content-Type: text/x-component
. They prepared a Flask server that responded with 200 and that header to satisfy the first request, and then the server sent the next internal request to an internal page and rendered it back to the attacker. This vulnerability was mitigated in v14.1.1.If the developer enables productionBrowserSourceMaps
in the configuration, JavaScript chunks contain mappings between action hashes and their original function names. In this case, you can use this excellent Burp plugin, https://github.com/Adversis/NextjsServerActionAnalyzer,
which you can use to enumerate all used and unused Next Action hashes mapped to their functions in the JavaScript code.
Next.js as a framework is shipped via npm as packages, next
, react
, react-dom
, and many transitive packages. The Next.js package itself depends on many other npm libraries for routing, bundling, server utilities, and more. If any of these public or private packages are deleted, or there is a typo in a package name, an attacker could upload a malicious package to the same registry with the same name. When the application updates dependencies, it may include the malicious package, and the impact can be severe, including remote code execution.
If you are testing any Next.js application, add these files to your directory brute forcing wordlist.
package.json
, package-lock.json
, yarn.lock
, .yarnrc.yml
, pnpm-lock.yaml
, pnpm-workspace.yaml
, and .npmrc
.
If any of these files are accessible to the public, you can download them and use this tool, https://github.com/visma-prodsec/confused, to quickly enumerate whether each package exists in the public registry and generate a report for each package in the file.
Next.js is not immune by default to CSRF attacks, so developers must implement defense in depth, such as:
So you are free to test this vulnerability on any Next.js application.
You can test some Next.js vulnerabilities using the lab in this repository.
https://github.com/upleveled/security-vulnerability-examples-next-js-postgres
I hope you learned something from this writeup. Regards.
Daoud Youssef, Technical Team Leader at DeepStrike
Stay secure with DeepStrike penetration testing services. Reach out for a quote or customized technical proposal today
Contact Us