VYPR
Medium severity5.3NVD Advisory· Published Apr 3, 2025· Updated Apr 15, 2026

CVE-2025-31486

CVE-2025-31486

Description

Vite is a frontend tooling framework for javascript. The contents of arbitrary files can be returned to the browser. By adding ?.svg with ?.wasm?init or with sec-fetch-dest: script header, the server.fs.deny restriction was able to bypass. This bypass is only possible if the file is smaller than build.assetsInlineLimit (default: 4kB) and when using Vite 6.0+. Only apps explicitly exposing the Vite dev server to the network (using --host or server.host config option) are affected. This vulnerability is fixed in 4.5.12, 5.4.17, 6.0.14, 6.1.4, and 6.2.5.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vitenpm
>= 6.2.0, < 6.2.56.2.5
vitenpm
>= 6.1.0, < 6.1.46.1.4
vitenpm
>= 6.0.0, < 6.0.146.0.14
vitenpm
>= 5.0.0, < 5.4.175.4.17
vitenpm
< 4.5.124.5.12

Patches

6
62d7e81ee189

fix: fs check with svg and relative paths (#19782)

https://github.com/vitejs/vitepatakApr 3, 2025via ghsa
7 files changed · +125 8
  • packages/vite/src/node/plugins/asset.ts+3 2 modified
    @@ -286,8 +286,9 @@ export async function fileToDevUrl(
     
       // If is svg and it's inlined in build, also inline it in dev to match
       // the behaviour in build due to quote handling differences.
    -  if (svgExtRE.test(id)) {
    -    const file = publicFile || cleanUrl(id)
    +  const cleanedId = cleanUrl(id)
    +  if (svgExtRE.test(cleanedId)) {
    +    const file = publicFile || cleanedId
         const content = await fsp.readFile(file)
         if (shouldInline(environment, file, id, content, undefined, undefined)) {
           return assetToDataURL(environment, file, content)
    
  • packages/vite/src/node/plugins/wasm.ts+3 1 modified
    @@ -3,6 +3,8 @@ import { fileToUrl } from './asset'
     
     const wasmHelperId = '\0vite/wasm-helper.js'
     
    +const wasmInitRE = /(?<![?#].*)\.wasm\?init/
    +
     const wasmHelper = async (opts = {}, url: string) => {
       let result
       if (url.startsWith('data:')) {
    @@ -63,7 +65,7 @@ export const wasmHelperPlugin = (): Plugin => {
               return `export default ${wasmHelperCode}`
             }
     
    -        if (!id.endsWith('.wasm?init')) {
    +        if (!wasmInitRE.test(id)) {
               return
             }
     
    
  • packages/vite/src/node/server/middlewares/transform.ts+30 5 modified
    @@ -1,5 +1,6 @@
     import path from 'node:path'
     import fsp from 'node:fs/promises'
    +import type { ServerResponse } from 'node:http'
     import type { Connect } from 'dep-types/connect'
     import colors from 'picocolors'
     import type { ExistingRawSourceMap } from 'rollup'
    @@ -16,7 +17,11 @@ import {
       removeTimestampQuery,
     } from '../../utils'
     import { send } from '../send'
    -import { ERR_LOAD_URL, transformRequest } from '../transformRequest'
    +import {
    +  ERR_DENIED_ID,
    +  ERR_LOAD_URL,
    +  transformRequest,
    +} from '../transformRequest'
     import { applySourcemapIgnoreList } from '../sourcemap'
     import { isHTMLProxy } from '../../plugins/html'
     import {
    @@ -47,6 +52,22 @@ const trailingQuerySeparatorsRE = /[?&]+$/
     const urlRE = /[?&]url\b/
     const rawRE = /[?&]raw\b/
     const inlineRE = /[?&]inline\b/
    +const svgRE = /\.svg\b/
    +
    +function deniedServingAccessForTransform(
    +  url: string,
    +  server: ViteDevServer,
    +  res: ServerResponse,
    +  next: Connect.NextFunction,
    +) {
    +  return (
    +    (rawRE.test(url) ||
    +      urlRE.test(url) ||
    +      inlineRE.test(url) ||
    +      svgRE.test(url)) &&
    +    !ensureServingAccess(url, server, res, next)
    +  )
    +}
     
     /**
      * A middleware that short-circuits the middleware chain to serve cached transformed modules
    @@ -178,10 +199,7 @@ export function transformMiddleware(
             '',
           )
           if (
    -        (rawRE.test(urlWithoutTrailingQuerySeparators) ||
    -          urlRE.test(urlWithoutTrailingQuerySeparators) ||
    -          inlineRE.test(urlWithoutTrailingQuerySeparators)) &&
    -        !ensureServingAccess(
    +        deniedServingAccessForTransform(
               urlWithoutTrailingQuerySeparators,
               server,
               res,
    @@ -231,6 +249,9 @@ export function transformMiddleware(
             // resolve, load and transform using the plugin container
             const result = await transformRequest(environment, url, {
               html: req.headers.accept?.includes('text/html'),
    +          allowId(id) {
    +            return !deniedServingAccessForTransform(id, server, res, next)
    +          },
             })
             if (result) {
               const depsOptimizer = environment.depsOptimizer
    @@ -301,6 +322,10 @@ export function transformMiddleware(
             // Let other middleware handle if we can't load the url via transformRequest
             return next()
           }
    +      if (e?.code === ERR_DENIED_ID) {
    +        // next() is called in ensureServingAccess
    +        return
    +      }
           return next(e)
         }
     
    
  • packages/vite/src/node/server/transformRequest.ts+11 0 modified
    @@ -32,6 +32,7 @@ import type { DevEnvironment } from './environment'
     
     export const ERR_LOAD_URL = 'ERR_LOAD_URL'
     export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
    +export const ERR_DENIED_ID = 'ERR_DENIED_ID'
     
     const debugLoad = createDebugger('vite:load')
     const debugTransform = createDebugger('vite:transform')
    @@ -55,6 +56,10 @@ export interface TransformOptions {
        * @internal
        */
       html?: boolean
    +  /**
    +   * @internal
    +   */
    +  allowId?: (id: string) => boolean
     }
     
     // TODO: This function could be moved to the DevEnvironment class.
    @@ -248,6 +253,12 @@ async function loadAndTransform(
     
       const moduleGraph = environment.moduleGraph
     
    +  if (options.allowId && !options.allowId(id)) {
    +    const err: any = new Error(`Denied ID ${id}`)
    +    err.code = ERR_DENIED_ID
    +    throw err
    +  }
    +
       let code: string | null = null
       let map: SourceDescription['map'] = null
     
    
  • playground/fs-serve/root/src/index.html+51 0 modified
    @@ -25,6 +25,8 @@ <h2>Unsafe Fetch</h2>
     <pre class="unsafe-fetch-8498-2"></pre>
     <pre class="unsafe-fetch-import-inline-status"></pre>
     <pre class="unsafe-fetch-raw-query-import-status"></pre>
    +<pre class="unsafe-fetch-query-dot-svg-import-status"></pre>
    +<pre class="unsafe-fetch-svg-status"></pre>
     
     <h2>Safe /@fs/ Fetch</h2>
     <pre class="safe-fs-fetch-status"></pre>
    @@ -49,13 +51,15 @@ <h2>Unsafe /@fs/ Fetch</h2>
     <pre class="unsafe-fs-fetch-8498-2"></pre>
     <pre class="unsafe-fs-fetch-import-inline-status"></pre>
     <pre class="unsafe-fs-fetch-import-inline-wasm-init-status"></pre>
    +<pre class="unsafe-fs-fetch-relative-path-after-query-status"></pre>
     
     <h2>Nested Entry</h2>
     <pre class="nested-entry"></pre>
     
     <h2>Denied</h2>
     <pre class="unsafe-dotenv"></pre>
     <pre class="unsafe-dotEnV-casing"></pre>
    +<pre class="unsafe-dotenv-query-dot-svg-wasm-init"></pre>
     
     <script type="module">
       import '../../entry'
    @@ -182,6 +186,24 @@ <h2>Denied</h2>
           console.error(e)
         })
     
    +  // outside of allowed dir with .svg query import
    +  fetch(joinUrlSegments(base, '/unsafe.txt?.svg?import'))
    +    .then((r) => {
    +      text('.unsafe-fetch-query-dot-svg-import-status', r.status)
    +    })
    +    .catch((e) => {
    +      console.error(e)
    +    })
    +
    +  // svg outside of allowed dir, treated as unsafe
    +  fetch(joinUrlSegments(base, '/unsafe.svg?import'))
    +    .then((r) => {
    +      text('.unsafe-fetch-svg-status', r.status)
    +    })
    +    .catch((e) => {
    +      console.error(e)
    +    })
    +
       // imported before, should be treated as safe
       fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/safe.json'))
         .then((r) => {
    @@ -298,6 +320,21 @@ <h2>Denied</h2>
           console.error(e)
         })
     
    +  // outside of root with relative path after query
    +  fetch(
    +    joinUrlSegments(
    +      base,
    +      joinUrlSegments('/@fs/', ROOT) +
    +        '/root/src/?/../../unsafe.txt?import&raw',
    +    ),
    +  )
    +    .then((r) => {
    +      text('.unsafe-fs-fetch-relative-path-after-query-status', r.status)
    +    })
    +    .catch((e) => {
    +      console.error(e)
    +    })
    +
       // outside root with special characters #8498
       fetch(
         joinUrlSegments(
    @@ -368,6 +405,20 @@ <h2>Denied</h2>
           console.error(e)
         })
     
    +  // .env with .svg?.wasm?init
    +  fetch(
    +    joinUrlSegments(
    +      base,
    +      joinUrlSegments('/@fs/', ROOT) + '/root/src/.env?.svg?.wasm?init',
    +    ),
    +  )
    +    .then((r) => {
    +      text('.unsafe-dotenv-query-dot-svg-wasm-init', r.status)
    +    })
    +    .catch((e) => {
    +      console.error(e)
    +    })
    +
       function text(sel, text) {
         document.querySelector(sel).textContent = text
       }
    
  • playground/fs-serve/root/unsafe.svg+3 0 added
    @@ -0,0 +1,3 @@
    +<svg height="100" width="100" xmlns="http://www.w3.org/2000/svg">
    +  <circle r="45" cx="50" cy="50" fill="purple" />
    +</svg>
    
  • playground/fs-serve/__tests__/fs-serve.spec.ts+24 0 modified
    @@ -79,6 +79,16 @@ describe.runIf(isServe)('main', () => {
         ).toBe('403')
       })
     
    +  test('unsafe fetch ?.svg?import', async () => {
    +    expect(
    +      await page.textContent('.unsafe-fetch-query-dot-svg-import-status'),
    +    ).toBe('403')
    +  })
    +
    +  test('unsafe fetch .svg?import', async () => {
    +    expect(await page.textContent('.unsafe-fetch-svg-status')).toBe('403')
    +  })
    +
       test('safe fs fetch', async () => {
         expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
         expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
    @@ -144,6 +154,14 @@ describe.runIf(isServe)('main', () => {
         ).toBe('403')
       })
     
    +  test('unsafe fs fetch with relative path after query status', async () => {
    +    expect(
    +      await page.textContent(
    +        '.unsafe-fs-fetch-relative-path-after-query-status',
    +      ),
    +    ).toBe('403')
    +  })
    +
       test('nested entry', async () => {
         expect(await page.textContent('.nested-entry')).toBe('foobar')
       })
    @@ -157,6 +175,12 @@ describe.runIf(isServe)('main', () => {
         const code = await page.textContent('.unsafe-dotEnV-casing')
         expect(code === '403' || code === '404').toBeTruthy()
       })
    +
    +  test('denied env with ?.svg?.wasm?init', async () => {
    +    expect(
    +      await page.textContent('.unsafe-dotenv-query-dot-svg-wasm-init'),
    +    ).toBe('403')
    +  })
     })
     
     describe('fetch', () => {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.