Skip to content

Commit 4627afb

Browse files
committed
fix: only serve files under src (#195)
1 parent 08cafec commit 4627afb

File tree

6 files changed

+78
-8
lines changed

6 files changed

+78
-8
lines changed

.changeset/long-pillows-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'vite-plugin-static-copy': patch
3+
---
4+
5+
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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ typings/
5454
# Yarn Integrity file
5555
.yarn-integrity
5656

57-
# dotenv environment variables file
58-
.env
59-
6057
# parcel-bundler cache (https://parceljs.org/)
6158
.cache
6259

src/middleware.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
OutgoingHttpHeaders,
1919
ServerResponse
2020
} from 'node:http'
21-
import { join, resolve } from 'node:path'
21+
import { join, resolve, sep } from 'node:path'
2222
import type { FileMap } from './serve'
2323
import type { TransformOptionObject } from './options'
2424
import {
@@ -54,6 +54,13 @@ function shouldServeOverwriteCheck(
5454
return true
5555
}
5656

57+
function isFileInside(filepath: string, srcBase: string) {
58+
const srcBaseWithTrailingSlash = srcBase.endsWith(sep)
59+
? srcBase
60+
: `${srcBase}${sep}`
61+
return filepath.startsWith(srcBaseWithTrailingSlash)
62+
}
63+
5764
function viaLocal(
5865
root: string,
5966
publicDir: string,
@@ -87,7 +94,13 @@ function viaLocal(
8794
if (!uri.startsWith(dir)) continue
8895

8996
for (const val of vals) {
90-
const filepath = resolve(root, val.src, uri.slice(dir.length))
97+
const srcBase = resolve(root, val.src)
98+
const filepath = resolve(srcBase, uri.slice(dir.length))
99+
if (!isFileInside(filepath, srcBase)) {
100+
// uri includes non-normalized `../`
101+
return undefined
102+
}
103+
91104
const overwriteCheck = shouldServeOverwriteCheck(
92105
val.overwrite,
93106
filepath,

test/fixtures/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SHOULD_BE_HIDDEN=PRIVATE

test/tests.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,24 @@ import type { PreviewServer, ViteDevServer } from 'vite'
33
import { build, createServer, preview } from 'vite'
44
import fetch from 'node-fetch'
55
import { testcases } from './testcases'
6-
import { getConfig, loadFileContent, normalizeLineBreak } from './utils'
6+
import {
7+
getConfig,
8+
loadFileContent,
9+
normalizeLineBreak,
10+
sendRawRequest,
11+
} from './utils'
712
import type { AddressInfo } from 'node:net'
813

14+
const constructUrl = (server: ViteDevServer | PreviewServer, path: string) => {
15+
const port = (server.httpServer!.address() as AddressInfo).port
16+
return `http://localhost:${port}${path}`
17+
}
18+
919
const fetchFromServer = async (
1020
server: ViteDevServer | PreviewServer,
1121
path: string
1222
) => {
13-
const port = (server.httpServer!.address() as AddressInfo).port
14-
const url = `http://localhost:${port}${path}`
23+
const url = constructUrl(server, path)
1524
const res = await fetch(url)
1625
return res
1726
}
@@ -95,6 +104,15 @@ describe('serve', () => {
95104
)
96105
expect(res.headers.get('Cross-Origin-Opener-Policy')).toBe('same-origin')
97106
})
107+
108+
test.concurrent('disallow path traversal with ../', async () => {
109+
const res = await sendRawRequest(
110+
constructUrl(server, '/'),
111+
'/fixture1/foo.txt/../.env',
112+
)
113+
expect(res).not.toContain('SHOULD_BE_HIDDEN')
114+
expect(res).toContain('HTTP/1.1 404 Not Found')
115+
})
98116
})
99117
})
100118

test/utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'
22
import { fileURLToPath } from 'node:url'
33
import type { InlineConfig } from 'vite'
44
import { normalizePath } from 'vite'
5+
import net from 'node:net'
56

67
export const root = new URL('./fixtures/', import.meta.url)
78

@@ -31,3 +32,38 @@ export const loadFileContent = async (
3132

3233
export const normalizeLineBreak = (input: string) =>
3334
input.replace(/\r\n/g, '\n')
35+
36+
export const sendRawRequest = async (
37+
baseUrl: string,
38+
requestTarget: string,
39+
) => {
40+
return new Promise<string>((resolve, reject) => {
41+
const parsedUrl = new URL(baseUrl)
42+
43+
const buf: Buffer[] = []
44+
const client = net.createConnection(
45+
{ port: +parsedUrl.port, host: parsedUrl.hostname },
46+
() => {
47+
client.write(
48+
[
49+
`GET ${encodeURI(requestTarget)} HTTP/1.1`,
50+
`Host: ${parsedUrl.host}`,
51+
'Connection: Close',
52+
'\r\n',
53+
].join('\r\n'),
54+
)
55+
},
56+
)
57+
client.on('data', (data) => {
58+
buf.push(data)
59+
})
60+
client.on('end', (hadError: unknown) => {
61+
if (!hadError) {
62+
resolve(Buffer.concat(buf).toString())
63+
}
64+
})
65+
client.on('error', (err) => {
66+
reject(err)
67+
})
68+
})
69+
}

0 commit comments

Comments
 (0)