CVE-2024-31207: Insufficient Information (NVD-CWE-noinfo)
value: Vite (French word for "quick", pronounced /vit/, like "veet") is a frontend build tooling to improve the frontend development experience.`server.fs.deny` does not deny requests for patterns with directories. This vulnerability has been patched in version(s) 5.2.6, 5.1.7, 5.0.13, 4.5.3, 3.2.10 and 2.9.18.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-04T16:15Z
Repository: https://github.com/vitejs/vite
Language: TypeScript
Files changed: packages/vite/src/node/server/index.ts, playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts, playground/fs-serve/package.json, playground/fs-serve/root/src/deny/.deny, playground/fs-serve/root/src/deny/deny.txt, playground/fs-serve/root/vite.config-deny.js
Commit date: 2024-03-24 13:31:50 +0100
Commit message: fix: `fs.deny` with globs with directories (#16250)


Diff: diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts
index 6b254dab7..134a781b9 100644
--- a/packages/vite/src/node/server/index.ts
+++ b/packages/vite/src/node/server/index.ts
@@ -683,14 +683,23 @@ export async function _createServer(
     },
     _restartPromise: null,
     _importGlobMap: new Map(),
     _forceOptimizeOnRestart: false,
     _pendingRequests: new Map(),
-    _fsDenyGlob: picomatch(config.server.fs.deny, {
-      matchBase: true,
-      nocase: true,
-    }),
+    _fsDenyGlob: picomatch(
+      // matchBase: true does not work as it's documented
+      // https://github.com/micromatch/picomatch/issues/89
+      // convert patterns without `/` on our side for now
+      config.server.fs.deny.map((pattern) =>
+        pattern.includes('/') ? pattern : `**/${pattern}`,
+      ),
+      {
+        matchBase: false,
+        nocase: true,
+        dot: true,
+      },
+    ),
     _shortcutsOptions: undefined,
   }
 
   // maintain consistency with the server instance after restarting.
   const reflexServer = new Proxy(server, {
diff --git a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts
new file mode 100644
index 000000000..fb60922e8
--- /dev/null
+++ b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest'
+import { isServe, page, viteTestUrl } from '~utils'
+
+describe.runIf(isServe)('main', () => {
+  test('**/deny/** should deny src/deny/deny.txt', async () => {
+    const res = await page.request.fetch(
+      new URL('/src/deny/deny.txt', viteTestUrl).href,
+    )
+    expect(res.status()).toBe(403)
+  })
+  test('**/deny/** should deny src/deny/.deny', async () => {
+    const res = await page.request.fetch(
+      new URL('/src/deny/.deny', viteTestUrl).href,
+    )
+    expect(res.status()).toBe(403)
+  })
+})
diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json
index b66b79268..f71a082b8 100644
--- a/playground/fs-serve/package.json
+++ b/playground/fs-serve/package.json
@@ -8,8 +8,11 @@
     "build": "vite build root",
     "debug": "node --inspect-brk ../../packages/vite/bin/vite",
     "preview": "vite preview root",
     "dev:base": "vite root --config ./root/vite.config-base.js",
     "build:base": "vite build root --config ./root/vite.config-base.js",
-    "preview:base": "vite preview root --config ./root/vite.config-base.js"
+    "preview:base": "vite preview root --config ./root/vite.config-base.js",
+    "dev:deny": "vite root --config ./root/vite.config-deny.js",
+    "build:deny": "vite build root --config ./root/vite.config-deny.js",
+    "preview:deny": "vite preview root --config ./root/vite.config-deny.js"
   }
 }
diff --git a/playground/fs-serve/root/src/deny/.deny b/playground/fs-serve/root/src/deny/.deny
new file mode 100644
index 000000000..73bd39608
--- /dev/null
+++ b/playground/fs-serve/root/src/deny/.deny
@@ -0,0 +1 @@
+.deny
diff --git a/playground/fs-serve/root/src/deny/deny.txt b/playground/fs-serve/root/src/deny/deny.txt
new file mode 100644
index 000000000..f9df83416
--- /dev/null
+++ b/playground/fs-serve/root/src/deny/deny.txt
@@ -0,0 +1 @@
+deny
diff --git a/playground/fs-serve/root/vite.config-deny.js b/playground/fs-serve/root/vite.config-deny.js
new file mode 100644
index 000000000..27501c55f
--- /dev/null
+++ b/playground/fs-serve/root/vite.config-deny.js
@@ -0,0 +1,22 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  build: {
+    rollupOptions: {
+      input: {
+        main: path.resolve(__dirname, 'src/index.html'),
+      },
+    },
+  },
+  server: {
+    fs: {
+      strict: true,
+      allow: [path.resolve(__dirname, 'src')],
+      deny: ['**/deny/**'],
+    },
+  },
+  define: {
+    ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
+  },
+})

Vulnerable code:     _fsDenyGlob: picomatch(config.server.fs.deny, {
      matchBase: true,
      nocase: true,
    }),
    "preview:base": "vite preview root --config ./root/vite.config-base.js"
Fixed code:     _fsDenyGlob: picomatch(
      // matchBase: true does not work as it's documented
      // https://github.com/micromatch/picomatch/issues/89
      // convert patterns without `/` on our side for now
      config.server.fs.deny.map((pattern) =>
        pattern.includes('/') ? pattern : `**/${pattern}`,
      ),
      {
        matchBase: false,
        nocase: true,
        dot: true,
      },
    ),
import { describe, expect, test } from 'vitest'
import { isServe, page, viteTestUrl } from '~utils'

describe.runIf(isServe)('main', () => {
  test('**/deny/** should deny src/deny/deny.txt', async () => {
    const res = await page.request.fetch(
      new URL('/src/deny/deny.txt', viteTestUrl).href,
    )
    expect(res.status()).toBe(403)
  })
  test('**/deny/** should deny src/deny/.deny', async () => {
    const res = await page.request.fetch(
      new URL('/src/deny/.deny', viteTestUrl).href,
    )
    expect(res.status()).toBe(403)
  })
})
    "preview:base": "vite preview root --config ./root/vite.config-base.js",
    "dev:deny": "vite root --config ./root/vite.config-deny.js",
    "build:deny": "vite build root --config ./root/vite.config-deny.js",
    "preview:deny": "vite preview root --config ./root/vite.config-deny.js"
.deny
deny
import path from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: path.resolve(__dirname, 'src/index.html'),
      },
    },
  },
  server: {
    fs: {
      strict: true,
      allow: [path.resolve(__dirname, 'src')],
      deny: ['**/deny/**'],
    },
  },
  define: {
    ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
  },
})