VYPR
Medium severityOSV Advisory· Published Aug 21, 2025· Updated Apr 15, 2026

CVE-2025-57753

CVE-2025-57753

Description

vite-plugin-static-copy is rollup-plugin-copy for Vite with dev server support. Files not included in src are accessible with a crafted request. The vulnerability is fixed in 2.3.2 and 3.1.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vite-plugin-static-copynpm
>= 3.0.0, < 3.1.23.1.2
vite-plugin-static-copynpm
>= 0.4.3, < 2.3.22.3.2

Affected products

1

Patches

4
326a79fb29bd

chore: update versions (#197)

https://github.com/sapphi-red/vite-plugin-static-copygithub-actions[bot]Aug 21, 2025via osv
3 files changed · +7 6
  • CHANGELOG.md+6 0 modified
    @@ -1,5 +1,11 @@
     # vite-plugin-static-copy
     
    +## 2.3.2
    +
    +### Patch Changes
    +
    +- [`4627afb`](https://github.com/sapphi-red/vite-plugin-static-copy/commit/4627afb8582083eab733881d3d974e1c1f23997d) Thanks [@sapphi-red](https://github.com/sapphi-red)! - Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    +
     ## 2.3.1
     
     ### Patch Changes
    
  • .changeset/long-pillows-arrive.md+0 5 removed
    @@ -1,5 +0,0 @@
    ----
    -'vite-plugin-static-copy': patch
    ----
    -
    -Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "vite-plugin-static-copy",
    -  "version": "2.3.1",
    +  "version": "2.3.2",
       "description": "rollup-plugin-copy for vite with dev server support.",
       "type": "module",
       "main": "./dist/index.js",
    
edab809c0188

chore: update versions (#196)

https://github.com/sapphi-red/vite-plugin-static-copygithub-actions[bot]Aug 21, 2025via osv
3 files changed · +7 6
  • CHANGELOG.md+6 0 modified
    @@ -1,5 +1,11 @@
     # vite-plugin-static-copy
     
    +## 3.1.2
    +
    +### Patch Changes
    +
    +- [#195](https://github.com/sapphi-red/vite-plugin-static-copy/pull/195) [`0bc6b49`](https://github.com/sapphi-red/vite-plugin-static-copy/commit/0bc6b49ed72b46eecfc9682045f4b46a19694969) Thanks [@sapphi-red](https://github.com/sapphi-red)! - Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    +
     ## 3.1.1
     
     ### Patch Changes
    
  • .changeset/long-pillows-arrive.md+0 5 removed
    @@ -1,5 +0,0 @@
    ----
    -'vite-plugin-static-copy': patch
    ----
    -
    -Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "vite-plugin-static-copy",
    -  "version": "3.1.1",
    +  "version": "3.1.2",
       "description": "rollup-plugin-copy for vite with dev server support.",
       "type": "module",
       "main": "./dist/index.js",
    
4627afb85820

fix: only serve files under `src` (#195)

6 files changed · +78 8
  • .changeset/long-pillows-arrive.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'vite-plugin-static-copy': patch
    +---
    +
    +Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    
  • .gitignore+0 3 modified
    @@ -54,9 +54,6 @@ typings/
     # Yarn Integrity file
     .yarn-integrity
     
    -# dotenv environment variables file
    -.env
    -
     # parcel-bundler cache (https://parceljs.org/)
     .cache
     
    
  • src/middleware.ts+15 2 modified
    @@ -18,7 +18,7 @@ import type {
       OutgoingHttpHeaders,
       ServerResponse
     } from 'node:http'
    -import { join, resolve } from 'node:path'
    +import { join, resolve, sep } from 'node:path'
     import type { FileMap } from './serve'
     import type { TransformOptionObject } from './options'
     import {
    @@ -54,6 +54,13 @@ function shouldServeOverwriteCheck(
       return true
     }
     
    +function isFileInside(filepath: string, srcBase: string) {
    +  const srcBaseWithTrailingSlash = srcBase.endsWith(sep)
    +    ? srcBase
    +    : `${srcBase}${sep}`
    +  return filepath.startsWith(srcBaseWithTrailingSlash)
    +}
    +
     function viaLocal(
       root: string,
       publicDir: string,
    @@ -87,7 +94,13 @@ function viaLocal(
         if (!uri.startsWith(dir)) continue
     
         for (const val of vals) {
    -      const filepath = resolve(root, val.src, uri.slice(dir.length))
    +      const srcBase = resolve(root, val.src)
    +      const filepath = resolve(srcBase, uri.slice(dir.length))
    +      if (!isFileInside(filepath, srcBase)) {
    +        // uri includes non-normalized `../`
    +        return undefined
    +      }
    +
           const overwriteCheck = shouldServeOverwriteCheck(
             val.overwrite,
             filepath,
    
  • test/fixtures/.env+1 0 added
    @@ -0,0 +1 @@
    +SHOULD_BE_HIDDEN=PRIVATE
    
  • test/tests.test.ts+21 3 modified
    @@ -3,15 +3,24 @@ import type { PreviewServer, ViteDevServer } from 'vite'
     import { build, createServer, preview } from 'vite'
     import fetch from 'node-fetch'
     import { testcases } from './testcases'
    -import { getConfig, loadFileContent, normalizeLineBreak } from './utils'
    +import {
    +  getConfig,
    +  loadFileContent,
    +  normalizeLineBreak,
    +  sendRawRequest,
    +} from './utils'
     import type { AddressInfo } from 'node:net'
     
    +const constructUrl = (server: ViteDevServer | PreviewServer, path: string) => {
    +  const port = (server.httpServer!.address() as AddressInfo).port
    +  return `http://localhost:${port}${path}`
    +}
    +
     const fetchFromServer = async (
       server: ViteDevServer | PreviewServer,
       path: string
     ) => {
    -  const port = (server.httpServer!.address() as AddressInfo).port
    -  const url = `http://localhost:${port}${path}`
    +  const url = constructUrl(server, path)
       const res = await fetch(url)
       return res
     }
    @@ -95,6 +104,15 @@ describe('serve', () => {
           )
           expect(res.headers.get('Cross-Origin-Opener-Policy')).toBe('same-origin')
         })
    +
    +    test.concurrent('disallow path traversal with ../', async () => {
    +      const res = await sendRawRequest(
    +        constructUrl(server, '/'),
    +        '/fixture1/foo.txt/../.env',
    +      )
    +      expect(res).not.toContain('SHOULD_BE_HIDDEN')
    +      expect(res).toContain('HTTP/1.1 404 Not Found')
    +    })
       })
     })
     
    
  • test/utils.ts+36 0 modified
    @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'
     import { fileURLToPath } from 'node:url'
     import type { InlineConfig } from 'vite'
     import { normalizePath } from 'vite'
    +import net from 'node:net'
     
     export const root = new URL('./fixtures/', import.meta.url)
     
    @@ -31,3 +32,38 @@ export const loadFileContent = async (
     
     export const normalizeLineBreak = (input: string) =>
       input.replace(/\r\n/g, '\n')
    +
    +export const sendRawRequest = async (
    +  baseUrl: string,
    +  requestTarget: string,
    +) => {
    +  return new Promise<string>((resolve, reject) => {
    +    const parsedUrl = new URL(baseUrl)
    +
    +    const buf: Buffer[] = []
    +    const client = net.createConnection(
    +      { port: +parsedUrl.port, host: parsedUrl.hostname },
    +      () => {
    +        client.write(
    +          [
    +            `GET ${encodeURI(requestTarget)} HTTP/1.1`,
    +            `Host: ${parsedUrl.host}`,
    +            'Connection: Close',
    +            '\r\n',
    +          ].join('\r\n'),
    +        )
    +      },
    +    )
    +    client.on('data', (data) => {
    +      buf.push(data)
    +    })
    +    client.on('end', (hadError: unknown) => {
    +      if (!hadError) {
    +        resolve(Buffer.concat(buf).toString())
    +      }
    +    })
    +    client.on('error', (err) => {
    +      reject(err)
    +    })
    +  })
    +}
    
0bc6b49ed72b

fix: only serve files under `src` (#195)

6 files changed · +78 8
  • .changeset/long-pillows-arrive.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'vite-plugin-static-copy': patch
    +---
    +
    +Files not included in `src` was possible to acess with a crafted request. See [GHSA-pp7p-q8fx-2968](https://github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968) for more details.
    
  • .gitignore+0 3 modified
    @@ -54,9 +54,6 @@ typings/
     # Yarn Integrity file
     .yarn-integrity
     
    -# dotenv environment variables file
    -.env
    -
     # parcel-bundler cache (https://parceljs.org/)
     .cache
     
    
  • src/middleware.ts+15 2 modified
    @@ -18,7 +18,7 @@ import type {
       OutgoingHttpHeaders,
       ServerResponse,
     } from 'node:http'
    -import { join, resolve } from 'node:path'
    +import { join, resolve, sep } from 'node:path'
     import type { FileMap } from './serve'
     import type { TransformOptionObject } from './options'
     import {
    @@ -54,6 +54,13 @@ function shouldServeOverwriteCheck(
       return true
     }
     
    +function isFileInside(filepath: string, srcBase: string) {
    +  const srcBaseWithTrailingSlash = srcBase.endsWith(sep)
    +    ? srcBase
    +    : `${srcBase}${sep}`
    +  return filepath.startsWith(srcBaseWithTrailingSlash)
    +}
    +
     function viaLocal(
       root: string,
       publicDir: string,
    @@ -87,7 +94,13 @@ function viaLocal(
         if (!uri.startsWith(dir)) continue
     
         for (const val of vals) {
    -      const filepath = resolve(root, val.src, uri.slice(dir.length))
    +      const srcBase = resolve(root, val.src)
    +      const filepath = resolve(srcBase, uri.slice(dir.length))
    +      if (!isFileInside(filepath, srcBase)) {
    +        // uri includes non-normalized `../`
    +        return undefined
    +      }
    +
           const overwriteCheck = shouldServeOverwriteCheck(
             val.overwrite,
             filepath,
    
  • test/fixtures/.env+1 0 added
    @@ -0,0 +1 @@
    +SHOULD_BE_HIDDEN=PRIVATE
    
  • test/tests.test.ts+21 3 modified
    @@ -2,15 +2,24 @@ import { describe, test, beforeAll, afterAll, expect } from 'vitest'
     import type { PreviewServer, ViteDevServer } from 'vite'
     import { build, createServer, preview } from 'vite'
     import { testcases } from './testcases'
    -import { getConfig, loadFileContent, normalizeLineBreak } from './utils'
    +import {
    +  getConfig,
    +  loadFileContent,
    +  normalizeLineBreak,
    +  sendRawRequest,
    +} from './utils'
     import type { AddressInfo } from 'node:net'
     
    +const constructUrl = (server: ViteDevServer | PreviewServer, path: string) => {
    +  const port = (server.httpServer!.address() as AddressInfo).port
    +  return `http://localhost:${port}${path}`
    +}
    +
     const fetchFromServer = async (
       server: ViteDevServer | PreviewServer,
       path: string,
     ) => {
    -  const port = (server.httpServer!.address() as AddressInfo).port
    -  const url = `http://localhost:${port}${path}`
    +  const url = constructUrl(server, path)
       const res = await fetch(url)
       return res
     }
    @@ -94,6 +103,15 @@ describe('serve', () => {
           )
           expect(res.headers.get('Cross-Origin-Opener-Policy')).toBe('same-origin')
         })
    +
    +    test.concurrent('disallow path traversal with ../', async () => {
    +      const res = await sendRawRequest(
    +        constructUrl(server, '/'),
    +        '/fixture1/foo.txt/../.env',
    +      )
    +      expect(res).not.toContain('SHOULD_BE_HIDDEN')
    +      expect(res).toContain('HTTP/1.1 404 Not Found')
    +    })
       })
     })
     
    
  • test/utils.ts+36 0 modified
    @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'
     import { fileURLToPath } from 'node:url'
     import type { InlineConfig } from 'vite'
     import { normalizePath } from 'vite'
    +import net from 'node:net'
     
     export const root = new URL('./fixtures/', import.meta.url)
     
    @@ -31,3 +32,38 @@ export const loadFileContent = async (
     
     export const normalizeLineBreak = (input: string) =>
       input.replace(/\r\n/g, '\n')
    +
    +export const sendRawRequest = async (
    +  baseUrl: string,
    +  requestTarget: string,
    +) => {
    +  return new Promise<string>((resolve, reject) => {
    +    const parsedUrl = new URL(baseUrl)
    +
    +    const buf: Buffer[] = []
    +    const client = net.createConnection(
    +      { port: +parsedUrl.port, host: parsedUrl.hostname },
    +      () => {
    +        client.write(
    +          [
    +            `GET ${encodeURI(requestTarget)} HTTP/1.1`,
    +            `Host: ${parsedUrl.host}`,
    +            'Connection: Close',
    +            '\r\n',
    +          ].join('\r\n'),
    +        )
    +      },
    +    )
    +    client.on('data', (data) => {
    +      buf.push(data)
    +    })
    +    client.on('end', (hadError: unknown) => {
    +      if (!hadError) {
    +        resolve(Buffer.concat(buf).toString())
    +      }
    +    })
    +    client.on('error', (err) => {
    +      reject(err)
    +    })
    +  })
    +}
    

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

7

News mentions

0

No linked articles in our index yet.