XSS bug in next.js router.push function!

mobin yardim
5 min readApr 12, 2024

In the final hours of my workday, our security team leader, Nader, brought to my attention an XSS (Cross-Site Scripting) vulnerability discovered in one of our front-end projects built with Next.js. As I wasn’t the primary developer on this project, it took me a while to grasp the nature of the attack.

Upon closer examination, I realized that the XSS vulnerability stemmed from the router.push function within our Next.js application. Both the router for the app directory and the page directory were unable to properly sanitize the path field. This allowed malicious code to be injected using the javacript:{some js code} schema, which Next.js erroneously interpreted as a valid URL schema.

Consequently, users navigating to URLs that contains javacript:{some js code}, inadvertently execute the embedded JavaScript code instead of being directed to the intended path.

Join me as I delve into the details of how we identified and addressed this vulnerability, and what it means for the future security of our site.

Potential Scenario for an XSS Attack Based on Reported Vulnerability

Imagine you have a website where certain pages are restricted to authorized users only. When a non-authorized user attempts to access one of these pages, the frontend application redirects them to an authorization page, appending a search parameter to the URL for redirection after successful authorization.

The codes for this scenario will be something like the codes below. By the way, I’ve created a demo project for this, available on GitHub.

middleware.ts:

export function middleware(request: NextRequest) {
const loginState = request.cookies.get("login-state")
if (!loginState?.value) {
const url = new URL('/auth', request.url);
const params = new URLSearchParams(url.search);
params.set("redirect-path", request.nextUrl.pathname)
url.search = params.toString();
return NextResponse.redirect(url)
} else {
return NextResponse.next();
}
}

export const config = {
matcher: [
"/dashboard",
"/my-wallet",
],
}

and the login page is something like the code below:

export default function Auth() {
const searchParams = useSearchParams()
const {push} = useRouter()

return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1>{"Auth"}</h1>
<form
onSubmit={async (event) => {
event.preventDefault();
window.document.cookie = "login-state=myCookieValue;"
push(searchParams.get("redirect-path") ?? "/")
}}
className={"flex flex-col gap-2"}
>
<input type={"email"} className={"text-black"}/>
<input type={"password"} className={"text-black"}/>
<button type={"submit"}>
{"login"}
</button>
</form>
</main>
);
}

If the attacker generates a URL with the following format:

website.com/auth?redirect-url=javascript:{url encoded js code}

and then sends it to the site admin or some site user, the following sequence of events may occur: initially, the user sees the login page for our website. Upon successful login, the router.push('javascript:{url encoded js code}')function will be executed, causing the injected JavaScript code within the URL to run. Consequently, the attacker can easily send a destructive request with the user session, retrieving the user token saved in local storage, and sending it to their endpoint.

Solutions

As a primary resolution, I propose that Next.js address this issue by enhancing its path handling to incorporate effective escaping using the “/” character. Frameworks must take responsibility for sanitizing user inputs to uphold robust security measures. By adopting this approach, not only we reinforce the integrity of our application, but we also instill confidence in our users, promising a more seamless and secure browsing experience overall.

If your code is vulnerable to the mentioned issue, you can temporarily mitigate it by manually sanitizing your redirect URLs, ensuring they start with a /. This simple trick can help protect your website from potential XSS attacks.

Here’s the updated code for the authentication page mentioned previously:

export default function Auth() {
const searchParams = useSearchParams()
const {push} = useRouter()

return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1>{"Auth"}</h1>
<form
onSubmit={async (event) => {
event.preventDefault();
window.document.cookie = "login-state=myCookieValue;"
push(`/${searchParams.get("redirect-path")}` ?? "/")
}}
className={"flex flex-col gap-2"}
>
<input type={"email"} className={"text-black"}/>
<input type={"password"} className={"text-black"}/>
<button type={"submit"}>
{"login"}
</button>
</form>
</main>
);
}

Another workaround is to utilize the ‘Referer’ header for redirection instead of passing URLs via search parameters. The ‘Referer’ header is handled by the browser, making it more secure as attackers would not be able to manipulate it to execute malicious JavaScript. By leveraging this header, we can enhance the security of our redirection mechanism and mitigate the risk of XSS attacks.

Engaging Next.js: Reporting XSS Vulnerabilities for Enhanced Security

After discovering the XSS vulnerability within Next.js, I promptly reported it to Vercel’s security team via responsible.disclosure@vercel.com, adhering to responsible disclosure practices. Within two days, I received acknowledgment of my report. However, after three weeks passed without any updates, I followed up with Vercel for progress. Their response indicated the necessity to report the issue through their integrity platform. While the initial communication demonstrated prompt acknowledgment, the subsequent redirection to another reporting channel highlighted the importance of clear and efficient bug reporting procedures in facilitating timely resolution of security concerns.

After 2 days of conversation in the Integrity platform, they informed me that the bug had been duplicated and previously reported on 2023/01/20.

It’s quite ironic that a company like Vercel, known for its framework’s robustness, had an XSS bug in their system. What’s even more surprising is their current focus — enabling frontend developers to execute database queries within button onclick listeners. This situation underscores the critical importance of transparent and efficient bug reporting processes to ensure the timely resolution of security vulnerabilities

I’d appreciate it if you could give my repository a star and share this article. If you’d like to connect with me, feel free to reach out on my LinkedIn profile!

--

--