Vite 6.1.4 is a minor update to the popular web development build tool, building upon version 6.1.3. Both versions share the same core dependencies like Rollup, esbuild, and PostCSS, ensuring consistent build performance and compatibility. The development dependencies, which aid in Vite's development and testing, also remain virtually identical, indicating a focus on stability and refinement rather than a major overhaul.
While the core functionality stays consistent, subtle differences exist. Notably, the "dist" object, which describes the package's distribution details, reveals that Vite 6.1.4 has a slightly larger unpacked size (2875694 bytes) compared to 6.1.3 (2875168 bytes). This suggests minor code additions, optimizations, or updated assets within the newer version. The release date also confirms 6.1.4 is the more recent version.
For developers, this suggests that migrating from 6.1.3 to 6.1.4 should be a smooth process. Vite's consistent dependency structure and the minor nature of the update minimize the risk of breaking changes. The slight increase in unpacked size may hint at performance improvements, bug fixes, or new features that enhance the developer experience. The shared dependencies ensure compatibility with existing plugins and configurations, maintaining the tool's ease of integration into diverse web development workflows. Since only file size and release date are the only differences between the 2 versions it is possible to assume 6.1.4 is the better overall version of the library.
All the vulnerabilities related to the version 6.1.4 of the package
Vite has an server.fs.deny
bypass with an invalid request-target
The contents of arbitrary files can be returned to the browser if the dev server is running on Node or Bun.
Only apps with the following conditions are affected.
HTTP 1.1 spec (RFC 9112) does not allow #
in request-target
. Although an attacker can send such a request. For those requests with an invalid request-line
(it includes request-target
), the spec recommends to reject them with 400 or 301. The same can be said for HTTP 2 (ref1, ref2, ref3).
On Node and Bun, those requests are not rejected internally and is passed to the user land. For those requests, the value of http.IncomingMessage.url
contains #
. Vite assumed req.url
won't contain #
when checking server.fs.deny
, allowing those kinds of requests to bypass the check.
On Deno, those requests are not rejected internally and is passed to the user land as well. But for those requests, the value of http.IncomingMessage.url
did not contain #
.
npm create vite@latest
cd vite-project/
npm install
npm run dev
send request to read /etc/passwd
curl --request-target /@fs/Users/doggy/Desktop/vite-project/#/../../../../../etc/passwd http://127.0.0.1:5173
Vite's server.fs.deny bypassed with /. for files under project root
The contents of files in the project root
that are denied by a file matching pattern can be returned to the browser.
Only apps explicitly exposing the Vite dev server to the network (using --host or server.host config option) are affected.
Only files that are under project root
and are denied by a file matching pattern can be bypassed.
.env
, .env.*
, *.{crt,pem}
, **/.env
**/.git/**
, .git/**
, .git/**/*
server.fs.deny
can contain patterns matching against files (by default it includes .env
, .env.*
, *.{crt,pem}
as such patterns).
These patterns were able to bypass for files under root
by using a combination of slash and dot (/.
).
npm create vite@latest
cd vite-project/
cat "secret" > .env
npm install
npm run dev
curl --request-target /.env/. http://localhost:5173
Vite middleware may serve files starting with the same name with the public directory
Files starting with the same name with the public directory were served bypassing the server.fs
settings.
Only apps that match the following conditions are affected:
server.host
config option)The servePublicMiddleware function is in charge of serving public files from the server. It returns the viteServePublicMiddleware function which runs the needed tests and serves the page. The viteServePublicMiddleware function checks if the publicFiles variable is defined, and then uses it to determine if the requested page is public. In the case that the publicFiles is undefined, the code will treat the requested page as a public page, and go on with the serving function. publicFiles may be undefined if there is a symbolic link anywhere inside the public directory. In that case, every requested page will be passed to the public serving function. The serving function is based on the sirv library. Vite patches the library to add the possibility to test loading access to pages, but when the public page middleware disables this functionality since public pages are meant to be available always, regardless of whether they are in the allow or deny list.
In the case of public pages, the serving function is provided with the path to the public directory as a root directory. The code of the sirv library uses the join function to get the full path to the requested file. For example, if the public directory is "/www/public", and the requested file is "myfile", the code will join them to the string "/www/public/myfile". The code will then pass this string to the normalize function. Afterwards, the code will use the string's startsWith function to determine whether the created path is within the given directory or not. Only if it is, it will be served.
Since sirv trims the trailing slash of the public directory, the string's startsWith function may return true even if the created path is not within the public directory. For example, if the server's root is at "/www", and the public directory is at "/www/p", if the created path will be "/www/private.txt", the startsWith function will still return true, because the string "/www/private.txt" starts with "/www/p". To achieve this, the attacker will use ".." to ask for the file "../private.txt". The code will then join it to the "/www/p" string, and will receive "/www/p/../private.txt". Then, the normalize function will return "/www/private.txt", which will then be passed to the startsWith function, which will return true, and the processing of the page will continue without checking the deny list (since this is the public directory middleware which doesn't check that).
Execute the following shell commands:
npm create vite@latest
cd vite-project/
mkdir p
cd p
ln -s a b
cd ..
echo 'import path from "node:path"; import { defineConfig } from "vite"; export default defineConfig({publicDir: path.resolve(__dirname, "p/"), server: {fs: {deny: [path.resolve(__dirname, "private.txt")]}}})' > vite.config.js
echo "secret" > private.txt
npm install
npm run dev
Then, in a different shell, run the following command:
curl -v --path-as-is 'http://localhost:5173/private.txt'
You will receive a 403 HTTP Response, because private.txt is denied.
Now in the same shell run the following command:
curl -v --path-as-is 'http://localhost:5173/../private.txt'
You will receive the contents of private.txt.
Vite's server.fs
settings were not applied to HTML files
Any HTML files on the machine were served regardless of the server.fs
settings.
Only apps that match the following conditions are affected:
appType: 'spa'
(default) or appType: 'mpa'
is usedThis vulnerability also affects the preview server. The preview server allowed HTML files not under the output directory to be served.
The serveStaticMiddleware function is in charge of serving static files from the server. It returns the viteServeStaticMiddleware function which runs the needed tests and serves the page. The viteServeStaticMiddleware function checks if the extension of the requested file is ".html". If so, it doesn't serve the page. Instead, the server will go on to the next middlewares, in this case htmlFallbackMiddleware, and then to indexHtmlMiddleware. These middlewares don't perform any test against allow or deny rules, and they don't make sure that the accessed file is in the root directory of the server. They just find the file and send back its contents to the client.
Execute the following shell commands:
npm create vite@latest
cd vite-project/
echo "secret" > /tmp/secret.html
npm install
npm run dev
Then, in a different shell, run the following command:
curl -v --path-as-is 'http://localhost:5173/../../../../../../../../../../../tmp/secret.html'
The contents of /tmp/secret.html will be returned.
This will also work for HTML files that are in the root directory of the project, but are in the deny list (or not in the allow list). Test that by stopping the running server (CTRL+C), and running the following commands in the server's shell:
echo 'import path from "node:path"; import { defineConfig } from "vite"; export default defineConfig({server: {fs: {deny: [path.resolve(__dirname, "secret_files/*")]}}})' > [vite.config.js](http://vite.config.js)
mkdir secret_files
echo "secret txt" > secret_files/secret.txt
echo "secret html" > secret_files/secret.html
npm run dev
Then, in a different shell, run the following command:
curl -v --path-as-is 'http://localhost:5173/secret_files/secret.txt'
You will receive a 403 HTTP Response, because everything in the secret_files directory is denied.
Now in the same shell run the following command:
curl -v --path-as-is 'http://localhost:5173/secret_files/secret.html'
You will receive the contents of secret_files/secret.html.
esbuild enables any website to send any requests to the development server and read the response
esbuild allows any websites to send any request to the development server and read the response due to default CORS settings.
esbuild sets Access-Control-Allow-Origin: *
header to all requests, including the SSE connection, which allows any websites to send any request to the development server and read the response.
https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/pkg/api/serve_other.go#L121 https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/pkg/api/serve_other.go#L363
Attack scenario:
http://malicious.example.com
).fetch('http://127.0.0.1:8000/main.js')
request by JS in that malicious web page. This request is normally blocked by same-origin policy, but that's not the case for the reasons above.http://127.0.0.1:8000/main.js
.In this scenario, I assumed that the attacker knows the URL of the bundle output file name. But the attacker can also get that information by
/index.html
: normally you have a script tag here/assets
: it's common to have a assets
directory when you have JS files and CSS files in a different directory and the directory listing feature tells the attacker the list of files/esbuild
SSE endpoint: the SSE endpoint sends the URL path of the changed files when the file is changed (new EventSource('/esbuild').addEventListener('change', e => console.log(e.type, e.data))
)The scenario above fetches the compiled content, but if the victim has the source map option enabled, the attacker can also get the non-compiled content by fetching the source map file.
npm i
npm run watch
fetch('http://127.0.0.1:8000/app.js').then(r => r.text()).then(content => console.log(content))
in a different website's dev tools.Users using the serve feature may get the source code stolen by malicious websites.