CVE-2023-34092: Use of Incorrectly-Resolved Name or Reference (CWE-706)
value: Vite provides frontend tooling. Prior to versions 2.9.16, 3.2.7, 4.0.5, 4.1.5, 4.2.3, and 4.3.9, Vite Server Options (`server.fs.deny`) can be bypassed using double forward-slash (//) allows any unauthenticated user to read file from the Vite root-path of the application including the default `fs.deny` settings (`['.env', '.env.*', '*.{crt,pem}']`). Only users explicitly exposing the Vite dev server to the network (using `--host` or `server.host` config option) are affected, and only files in the immediate Vite project root folder could be exposed. This issue is fixed in vite@4.3.9, vite@4.2.3, vite@4.1.5, vite@4.0.5, vite@3.2.7, and vite@2.9.16.
CWE description: The product uses a name or reference to access a resource, but the name/reference resolves to a resource that is outside of the intended control sphere.
CVSS v3 base score: 7.5
Published: 2023-06-01T17:15Z
Repository: https://github.com/vitejs/vite
Language: TypeScript
Files changed: packages/vite/src/node/server/middlewares/static.ts, playground/assets-sanitize/.env, playground/assets-sanitize/__tests__/assets-sanitize.spec.ts, playground/assets-sanitize/index.html
Commit date: 2023-05-26 10:17:52 +0000
Commit message: fix: fs.deny with leading double slash (#13348)


Diff: diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts
index 13904ce0..42aa48a1 100644
--- a/packages/vite/src/node/server/middlewares/static.ts
+++ b/packages/vite/src/node/server/middlewares/static.ts
@@ -98,11 +98,11 @@ export function serveStaticMiddleware(
       isInternalRequest(req.url!)
     ) {
       return next()
     }
 
-    const url = new URL(req.url!, 'http://example.com')
+    const url = new URL(req.url!.replace(/^\/+/, '/'), 'http://example.com')
     const pathname = decodeURIComponent(url.pathname)
 
     // apply aliases to static requests as well
     let redirectedPathname: string | undefined
     for (const { find, replacement } of server.config.resolve.alias) {
@@ -151,11 +151,11 @@ export function serveRawFsMiddleware(
     sirvOptions({ headers: server.config.server.headers }),
   )
 
   // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
   return function viteServeRawFsMiddleware(req, res, next) {
-    const url = new URL(req.url!, 'http://example.com')
+    const url = new URL(req.url!.replace(/^\/+/, '/'), 'http://example.com')
     // In some cases (e.g. linked monorepos) files outside of root will
     // reference assets that are also out of served root. In such cases
     // the paths are rewritten to `/@fs/` prefixed paths and must be served by
     // searching based from fs root.
     if (url.pathname.startsWith(FS_PREFIX)) {
diff --git a/playground/assets-sanitize/.env b/playground/assets-sanitize/.env
new file mode 100644
index 00000000..d0e0cfd2
--- /dev/null
+++ b/playground/assets-sanitize/.env
@@ -0,0 +1 @@
+KEY=unsafe
diff --git a/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts
index 9fd8b1e9..f4a25aa1 100644
--- a/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts
+++ b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts
@@ -23,5 +23,10 @@ if (!isBuild) {
     expect(underscoreCircleAsset).toMatch('/_circle')
     expect(plusCircleAsset).not.toEqual(underscoreCircleAsset)
     expect(Object.keys(manifest).length).toBe(3) // 2 svg, 1 index.js
   })
 }
+
+test.runIf(!isBuild)('denied .env', async () => {
+  expect(await page.textContent('.unsafe-dotenv')).toBe('403')
+  expect(await page.textContent('.unsafe-dotenv-double-slash')).toBe('403')
+})
diff --git a/playground/assets-sanitize/index.html b/playground/assets-sanitize/index.html
index e4b4913c..0e634695 100644
--- a/playground/assets-sanitize/index.html
+++ b/playground/assets-sanitize/index.html
@@ -4,8 +4,37 @@
     background-repeat: no-repeat;
     padding-left: 2rem;
     margin-bottom: 1rem;
   }
 </style>
-<h1>test elements below should show circles and their url</h1>
+<h3>test elements below should show circles and their url</h3>
 <div class="test-el plus-circle"></div>
 <div class="test-el underscore-circle"></div>
+
+<h3>Denied .env</h3>
+<div class="unsafe-dotenv"></div>
+<div class="unsafe-dotenv-double-slash"></div>
+
+<script type="module">
+  // .env, denied by default. See fs-serve playground for other fs tests
+  // these checks ensure that a project without a custom root respects fs.deny
+
+  fetch('/.env')
+    .then((r) => {
+      text('.unsafe-dotenv', r.status)
+    })
+    .catch((e) => {
+      console.error(e)
+    })
+
+  fetch(window.location + '/.env')
+    .then((r) => {
+      text('.unsafe-dotenv-double-slash', r.status)
+    })
+    .catch((e) => {
+      console.error(e)
+    })
+
+  function text(el, text) {
+    document.querySelector(el).textContent = text
+  }
+</script>

Vulnerable code:     const url = new URL(req.url!, 'http://example.com')
    const url = new URL(req.url!, 'http://example.com')
<h1>test elements below should show circles and their url</h1>
Fixed code:     const url = new URL(req.url!.replace(/^\/+/, '/'), 'http://example.com')
    const url = new URL(req.url!.replace(/^\/+/, '/'), 'http://example.com')
KEY=unsafe

test.runIf(!isBuild)('denied .env', async () => {
  expect(await page.textContent('.unsafe-dotenv')).toBe('403')
  expect(await page.textContent('.unsafe-dotenv-double-slash')).toBe('403')
})
<h3>test elements below should show circles and their url</h3>

<h3>Denied .env</h3>
<div class="unsafe-dotenv"></div>
<div class="unsafe-dotenv-double-slash"></div>

<script type="module">
  // .env, denied by default. See fs-serve playground for other fs tests
  // these checks ensure that a project without a custom root respects fs.deny

  fetch('/.env')
    .then((r) => {
      text('.unsafe-dotenv', r.status)
    })
    .catch((e) => {
      console.error(e)
    })

  fetch(window.location + '/.env')
    .then((r) => {
      text('.unsafe-dotenv-double-slash', r.status)
    })
    .catch((e) => {
      console.error(e)
    })

  function text(el, text) {
    document.querySelector(el).textContent = text
  }
</script>