Summary

I reported and coordinated CVE-2026-25149, a protocol-relative open redirect in Qwik City’s default request handler middleware. This was one of many vulnerabilities that I reported in the Qwik.js framework, but most of the others turned out to be duplicates of pending reports. I’ve been seeing a lot of protocol-relative vulnerabilities lately, so I’ll use this opportunity to highlight the vulnerability class and when exploitability is real.

Severity: Medium - 5.3 (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N)

An Open Redirect (CWE-601) vulnerability in Qwik City’s default request handler middleware allows a remote attacker to redirect users to arbitrary protocol-relative URLs. Successful exploitation permits attackers to craft convincing phishing links that appear to originate from the trusted domain but redirect the victim to an attacker-controlled site. This affects qwik-city before version 1.19.0. This has been patched in qwik-city version 1.19.0.

Technical Analysis

Qwik.js is an open-source front-end web framework. Qwik City serves as the meta-framework that provides application-level infrastructure, including routing, data loading, and layout management.

Protocol-relative URLs (like //websmite.com) are network-path references that instruct the browser to use the same URL handler scheme (http: or https:) as the current page when fetching a resource. In the context of a redirect, if an application sends a Location: //websmite.com header while the user is browsing via HTTPS, the browser interprets this as a valid directive to navigate to https://websmite.com.

This vulnerability lies in how HTTP request paths are translated. Specifically, a path like https://qwiksite.com//websmite.com can be redirected as Location: //websmite.com. However, there’s quite a bit of nuance to this. First and foremost, an arbitrary domain name in a path isn’t going to match a static route in a real-world website. To match an arbitrary domain, an open redirect like this typically requires catch-all routes. These are extremely common in modern sites, often seen in dynamic slugs and custom 404s among other framework logic. In Qwik, this looks something like a router directory named src/routes/[...slug]/index.tsx.

A diagram explaining the flow.

Qwik City depends on the hosting runtime (Node, Bun, Cloudflare, etc.) to normalize incoming URL paths. Some runtime adapters collapse repeated slashes (// to /), while others forward the raw path unchanged. Whether this bug is exploitable also depends on the deployment environment, since proxies or platforms may sanitize the path before it reaches Qwik. In environments and runtimes that preserve a leading //, Qwik City can generate a redirect to a protocol-relative URL.

To ensure URL consistency, Qwik City applies fixTrailingSlash() to page routes.

// qwik-city/middleware/request-handler/fix-trailing-slash.ts
function fixTrailingSlash(ev: RequestEvent) {
  const trailingSlash = getRequestTrailingSlash(ev);
  const { basePathname, originalUrl, sharedMap } = ev;
  const { pathname, search } = originalUrl;
  ...
    if (trailingSlash) {
      if (!pathname.endsWith('/')) {
        throw ev.redirect(HttpStatus.MovedPermanently, pathname + '/' + search);
      }
    } 
  ...
}

By default, fixTrailingSlash() does not validate that the pathname starts with a single slash. If the application is deployed on a runtime that passes raw, non-normalized URLs, a request path starting with double slashes (e.g. //websmite.com) is passed directly to the middleware. The middleware detects the missing slash at the end and blindly appends it, resulting in a redirect target of //websmite.com/.

The redirect function attempts to sanitize the target URL, but the regex used is insufficient. The logic relies on detecting a character preceding the double slashes.

// qwik-city/src/middleware/request-handler/request-event.ts
redirect: (statusCode: number, url: string) => {
  ...
  if (url) {
    if (/([^:])\/{2,}/.test(url)) {
      const fixedURL = url.replace(/([^:])\/{2,}/g, '$1/');
      url = fixedURL;
    }
    headers.set('Location', url);
  }
  ...
}

The regex /([^:])\/{2,}/ requires a character that is not a colon to precede the double slashes. Because a protocol-relative URL at the start of a string has no preceding character, the regex fails to match. The malicious URL bypasses the check and is set in the Location header.

The value used for the original unsafe URL depends entirely on how the specific runtime adapter normalizes incoming requests. If an adapter does something like new URL(request.url) where request.url is absolute, it may preserve the path as received.

For this bug class, the environment aspect boils down to two questions:

  1. Does the platform preserve an incoming path that begins with // all the way through to the framework and router as a pathname that still begins with //?
  2. When do framework adapters treat a proxy or header-provided URL as authoritative?

If either the platform collapses // before Qwik City sees it, or the adapter normalizes it, the redirect-to-protocol-relative target won’t resolve to the attacker’s benefit. Even if the platform and framework enable the vulnerability, there may be other environmental conditions, like proxies, that normalize paths, so real-world exploitability is still environment-dependent.

Below is an analysis of the Qwik City adapters. What the Qwik City adapter code cannot prove is whether the platform ingress (Bun/Deno/Edge providers, proxies, etc.) already normalizes // before the adapter ever sees it.

The Node adapter normalizes the incoming request path before routing, which prevents a leading // pathname from reaching fixTrailingSlash. The AWS Lambda and Firebase adapters wrap the Node adapter as well.

// qwik-city/src/middleware/node/http.ts
export function normalizeUrl(url: string, base: string) {
  const DOUBLE_SLASH_REG = /\/\/|\\\\/g;

  return new URL(url.replace(DOUBLE_SLASH_REG, '/'), base);
}

The Bun, Deno, Netlify Edge and Vercel Edge adapters use the standard Request object, which allows for the protocol-relative paths.

// qwik-city/src/middleware/bun/index.ts
async function router(request: Request) {
  try {
    const url = new URL(request.url);
    const serverRequestEv = {
      ...
      url,
      ...
    };
    ...
  }
}

The Azure middleware trusts a specific header, x-ms-original-url. If the upstream can manipulate this header, it is also vulnerable.

// qwik-city/src/middleware/azure-swa/index.ts
async function onAzureSwaRequest(context: Context, req: HttpRequest): Promise<AzureResponse> {
  try {
    const url = new URL(req.headers['x-ms-original-url']!);

    const serverRequestEv = {
       ...
       url,
       ...
    };
  }
}

The Cloudflare middleware uses the standard URL constructor, making the code itself vulnerable. Cloudflare may normalize the incoming request before it hits the worker.

// qwik-city/src/middleware/cloudflare-pages/index.ts
async function onCloudflarePagesFetch(...) {
  try {
    const url = new URL(request.url);
    ...
  }
}

While the runtime provides the raw input, the vulnerability lies in the framework middleware failing to validate or sanitize that input before triggering a redirect. But as this writeup demonstrates, the real-world exploitability of protocol-relative open redirects is very platform and environment-dependent. This is true for many frameworks outside of Qwik.

Impact

This vulnerability impacts Qwik City applications deployed to runtimes that do not automatically normalize URL paths and use a catch-all route to match arbitrary paths. This was successfully tested E2E in Bun.

By making an attacker-controlled destination look like a normal navigation from a trusted domain, framework-level open redirects enable convincing phishing and credential-harvesting campaigns. They also support techniques that can bypass URL allowlists, reputation checks, and other security filters, since the initial link points to a legitimate site. In more complex chains, open redirects can amplify other weaknesses like misconfigured SSO flows, weak callback or return-URL validation, or overly-trusting cookie and session assumptions, leading to token theft or login CSRF-style outcomes in systems that treat redirects as trusted transitions.

Despite these impacts, open redirects are typically scored in the low-to-medium CVSS range. This is because they rarely provide direct code execution or data exfiltration on their own, and usually require user interaction and additional attacker-controlled infrastructure. The most serious outcomes tend to depend on chaining with other misconfigurations or weaknesses.

Patches

This issue has been patched in Qwik version 1.19.0.

Developers can mitigate CVE-2026-25149 on the affected versions by ensuring paths are normalized before reaching a potentially vulnerable runtime adapter.

Timeline

DateEvent
2025-12-28Initial Report via GitHub Private Vulnerability Report
2026-01-29Maintainer Acknowledgement
2026-01-30Patch Released in 1.19.0
2026-01-30GitHub Staff Assigns CVE-2026-25149
2026-02-03Maintainer Releases GHSA-92j7-wgmg-f32m
2026-02-03Published to GitHub Advisory Database
2026-02-03Published to National Vulnerability Database
2026-02-03Published to websmite.com