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.
| Package | Affected versions | Patched versions |
|---|---|---|
vite-plugin-static-copynpm | >= 3.0.0, < 3.1.2 | 3.1.2 |
vite-plugin-static-copynpm | >= 0.4.3, < 2.3.2 | 2.3.2 |
Affected products
1- Range: v0.10.0, v0.11.0, v0.11.1, …
Patches
4326a79fb29bdchore: update versions (#197)
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",
edab809c0188chore: update versions (#196)
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",
4627afb85820fix: 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) + }) + }) +}
0bc6b49ed72bfix: 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- github.com/advisories/GHSA-pp7p-q8fx-2968ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-57753ghsaADVISORY
- github.com/sapphi-red/vite-plugin-static-copy/commit/0bc6b49ed72b46eecfc9682045f4b46a19694969ghsaWEB
- github.com/sapphi-red/vite-plugin-static-copy/commit/4627afb8582083eab733881d3d974e1c1f23997dghsaWEB
- github.com/sapphi-red/vite-plugin-static-copy/releases/tag/vite-plugin-static-copy%402.3.2ghsaWEB
- github.com/sapphi-red/vite-plugin-static-copy/releases/tag/vite-plugin-static-copy%403.1.2ghsaWEB
- github.com/sapphi-red/vite-plugin-static-copy/security/advisories/GHSA-pp7p-q8fx-2968nvdWEB
News mentions
0No linked articles in our index yet.