Project: Homarr (https://github.com/homarr-labs/homarr)
Vulnerability Type: DOM-Based Cross-Site Scripting (XSS)
Severity: High (CVSS 3.1 Base Score: 8.8)
Report Date: March 21, 2026
1. Summary
A DOM-based Cross-Site Scripting (XSS) vulnerability has been discovered in Homarr's /auth/login page. The application improperly trusts a URL parameter (callbackUrl), which is passed to redirect and router.push. An attacker can craft a malicious link that, when opened by an authenticated user, performs a client-side redirect and executes arbitrary JavaScript in the context of their browser. This could lead to credential theft, internal network pivoting, and unauthorized actions performed on behalf of the victim.
2. Vulnerability Details
2.1. Affected Component and Code
There are two code sinks which lead to XSS. The first is triggered if the user is already authenticated. The second is triggered if the user is not authenticated, but proceeds to submit a sign-in form.
// auth/login/page.tsx
const searchParams = await props.searchParams;
const session = await auth();
if (session) {
redirect(searchParams.callbackUrl ?? "/"); // <-- sink
}
// [...]
return (
// [...]
<LoginForm
providers={env.AUTH_PROVIDERS}
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
isOidcAutoLoginEnabled={env.AUTH_OIDC_AUTO_LOGIN}
callbackUrl={searchParams.callbackUrl ?? "/"} // <-- sink
/>
// auth/login/_login-form.tsx
export const LoginForm = ({ /* ... */ callbackUrl }: LoginFormProps) => {
const onSuccess = useCallback(
async (provider: Provider, response: Awaited<ReturnType<typeof signIn>>) => {
// [...]
router.push(callbackUrl); // <-- sink
},
[t, router, callbackUrl],
);
As shown, the callbackUrl parameter is taken from the browser's URL query and later passed to redirect or router.push without validating against the javascript: protocol, leading to XSS. redirect is called if a session exists (i.e. user is already authenticated), and router.push is called after an unauthenticated user successfully logs in.
This happens because redirect and router.push pass its parameter to window.location, which is known to parse and execute JavaScript in javascript: URIs. (Note: According to previous discussions, the stance from the NextJS maintainers is that library users are responsible for applying sanitisation for input passed to router.push and router.replace. See this NextJS GitHub issue.)
2.2. Steps to Reproduce and Proof of Concept (PoC)
-
Attacker crafts a malicious URL:
http://HOST:7575/auth/login?callbackUrl=javascript:alert(window.origin)
A sophisticated payload could hide the JavaScript by inserting extra query parameters or using URL encoding to obfuscate the attack, making the URL appear less suspicious to victims.
-
Victim Action: A victim clicks the link.
- If the victim is already authenticated, no further action is required and the XSS is triggered.
- If the victim is not authenticated, the XSS only triggers upon a successful login.
2.3. Screenshot
3. CVSS v3.1 Score Justification
Base Score: 8.8 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L
- Attack Vector (AV): Network (N) – The vulnerability is exploitable remotely over the network via a crafted URL.
- Attack Complexity (AC): Low (L) – The attack does not require complex conditions.
- Privileges Required (PR): None (N) – No authentication or privileges are required by the attacker to trigger the vulnerability.
- User Interaction (UI): Required (R) – The victim must click on the attacker's malicious link and, if unauthenticated, perform a signup action.
- Scope (S): Changed (C) – The vulnerable component is the client-side code, but the impact (executing arbitrary script) affects the user's browser session and the data accessible within the application's security context.
- Confidentiality (C): High (H) – Successful exploitation could lead to complete loss of confidentiality. An attacker can call authenticated API endpoints and other information stored in the browser's context.
- Integrity (I): Low (L) – An attacker could modify data or perform actions on behalf of the user.
- Availability (A): Low (L) – In a reasonable worst case scenario, an administrator is compromised, leading to potential actions which affect the availability of the system.
4. Impact
Successful exploitation of this vulnerability has a High impact:
- Arbitrary Actions: The script could use the victim's active session to make API requests, create, modify, or delete items, or perform any action the victim is authorized to do.
- Credential Theft: A script could present a fake login prompt within the context of the legitimate site to capture the user's credentials.
- Internal Network Pivoting: If the victim is within an internal network, the XSS could be used as a foothold to perform attacks against other internal systems from the victim's browser.
5. Remediation Recommendations
To fix this vulnerability, the application should validate and sanitise the callbackUrl variable before using it for redirection. Here are a few approaches:
5.1. Approach 1
This approach only allows absolute URL paths such as /abc and protects against open redirects such as //example.com/path.
if (callbackUrl && callbackUrl.startsWith("/") && !callbackUrl.startsWith("//")) {
redirectPath = callbackUrl;
} else {
redirectPath = '/'; // Safe default
}
5.2. Approach 2
This approach allows for http:// and https:// scheme in the redirect URL, but also protects against open redirect. Functionally, it is the same as Approach 1.
-
Validate the redirect path stays within the same origin and uses a safe scheme. Use the URL constructor to parse the path relative to the current origin and check both the origin and the protocol:
function isValidRedirectPath(path) {
try {
// Parse relative paths correctly by providing the current origin as the base
const url = new URL(path, window.location.origin);
// Ensure the origin matches AND the scheme is either http: or https:
return url.origin === window.location.origin &&
(url.protocol === 'http:' || url.protocol === 'https:');
} catch {
return false;
}
}
This validation correctly handles:
- Relative paths like
/dashboard (resolves to https://example.com/dashboard)
- Absolute paths within the same origin like
https://example.com/pipelines
- Blocks cross-origin and open redirects like
//attacker.site/index.html and https://attacker.site/
- Blocks dangerous schemes like
javascript:, data:, vbscript:, etc.
-
Apply validation before any navigation. If callbackUrl passes validation, it can be safely used; otherwise, fall back to a default safe path like /.
if (callbackUrl && isValidRedirectPath(callbackUrl)) {
redirectPath = callbackUrl;
} else {
redirectPath = '/'; // Safe default
}
6. Timeline and Disclosure Process
- 2026-03-21: Vulnerability discovered and this report prepared.
- 2026-03-21: Report sent to Homarr security contact / maintainers.
- 2026-03-21: Maintainer acknowledges receipt of the report and implements a fix.
- 2026-04-04: Maintainer publishes security advisory on GHSA.
Project: Homarr (https://github.com/homarr-labs/homarr)
Vulnerability Type: DOM-Based Cross-Site Scripting (XSS)
Severity: High (CVSS 3.1 Base Score: 8.8)
Report Date: March 21, 2026
1. Summary
A DOM-based Cross-Site Scripting (XSS) vulnerability has been discovered in Homarr's
/auth/loginpage. The application improperly trusts a URL parameter (callbackUrl), which is passed toredirectandrouter.push. An attacker can craft a malicious link that, when opened by an authenticated user, performs a client-side redirect and executes arbitrary JavaScript in the context of their browser. This could lead to credential theft, internal network pivoting, and unauthorized actions performed on behalf of the victim.2. Vulnerability Details
2.1. Affected Component and Code
There are two code sinks which lead to XSS. The first is triggered if the user is already authenticated. The second is triggered if the user is not authenticated, but proceeds to submit a sign-in form.
apps/nextjs/src/app/[locale]/auth/login/page.tsx, Line 22apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx, Line 73As shown, the
callbackUrlparameter is taken from the browser's URL query and later passed toredirectorrouter.pushwithout validating against thejavascript:protocol, leading to XSS.redirectis called if a session exists (i.e. user is already authenticated), androuter.pushis called after an unauthenticated user successfully logs in.This happens because
redirectandrouter.pushpass its parameter towindow.location, which is known to parse and execute JavaScript injavascript:URIs. (Note: According to previous discussions, the stance from the NextJS maintainers is that library users are responsible for applying sanitisation for input passed torouter.pushandrouter.replace. See this NextJS GitHub issue.)2.2. Steps to Reproduce and Proof of Concept (PoC)
Attacker crafts a malicious URL:
A sophisticated payload could hide the JavaScript by inserting extra query parameters or using URL encoding to obfuscate the attack, making the URL appear less suspicious to victims.
Victim Action: A victim clicks the link.
2.3. Screenshot
3. CVSS v3.1 Score Justification
Base Score: 8.8 (High)
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L4. Impact
Successful exploitation of this vulnerability has a High impact:
5. Remediation Recommendations
To fix this vulnerability, the application should validate and sanitise the
callbackUrlvariable before using it for redirection. Here are a few approaches:5.1. Approach 1
This approach only allows absolute URL paths such as
/abcand protects against open redirects such as//example.com/path.5.2. Approach 2
This approach allows for
http://andhttps://scheme in the redirect URL, but also protects against open redirect. Functionally, it is the same as Approach 1.Validate the redirect path stays within the same origin and uses a safe scheme. Use the
URLconstructor to parse the path relative to the current origin and check both the origin and the protocol:This validation correctly handles:
/dashboard(resolves tohttps://example.com/dashboard)https://example.com/pipelines//attacker.site/index.htmlandhttps://attacker.site/javascript:,data:,vbscript:, etc.Apply validation before any navigation. If
callbackUrlpasses validation, it can be safely used; otherwise, fall back to a default safe path like/.6. Timeline and Disclosure Process