Skip to content

Commit 28cffc9

Browse files
authored
fix: fs serve only edit pathname (fixes #9148) (#9173)
1 parent 2139ac1 commit 28cffc9

File tree

3 files changed

+59
-18
lines changed

3 files changed

+59
-18
lines changed

packages/vite/src/node/server/middlewares/static.ts

+24-18
Original file line numberDiff line numberDiff line change
@@ -80,36 +80,40 @@ export function serveStaticMiddleware(
8080
return next()
8181
}
8282

83-
const url = decodeURIComponent(req.url!)
83+
const url = new URL(req.url!, 'http://example.com')
84+
const pathname = decodeURIComponent(url.pathname)
8485

8586
// apply aliases to static requests as well
86-
let redirected: string | undefined
87+
let redirectedPathname: string | undefined
8788
for (const { find, replacement } of server.config.resolve.alias) {
8889
const matches =
89-
typeof find === 'string' ? url.startsWith(find) : find.test(url)
90+
typeof find === 'string'
91+
? pathname.startsWith(find)
92+
: find.test(pathname)
9093
if (matches) {
91-
redirected = url.replace(find, replacement)
94+
redirectedPathname = pathname.replace(find, replacement)
9295
break
9396
}
9497
}
95-
if (redirected) {
98+
if (redirectedPathname) {
9699
// dir is pre-normalized to posix style
97-
if (redirected.startsWith(dir)) {
98-
redirected = redirected.slice(dir.length)
100+
if (redirectedPathname.startsWith(dir)) {
101+
redirectedPathname = redirectedPathname.slice(dir.length)
99102
}
100103
}
101104

102-
const resolvedUrl = redirected || url
103-
let fileUrl = path.resolve(dir, resolvedUrl.replace(/^\//, ''))
104-
if (resolvedUrl.endsWith('/') && !fileUrl.endsWith('/')) {
105+
const resolvedPathname = redirectedPathname || pathname
106+
let fileUrl = path.resolve(dir, resolvedPathname.replace(/^\//, ''))
107+
if (resolvedPathname.endsWith('/') && !fileUrl.endsWith('/')) {
105108
fileUrl = fileUrl + '/'
106109
}
107110
if (!ensureServingAccess(fileUrl, server, res, next)) {
108111
return
109112
}
110113

111-
if (redirected) {
112-
req.url = encodeURIComponent(redirected)
114+
if (redirectedPathname) {
115+
url.pathname = encodeURIComponent(redirectedPathname)
116+
req.url = url.href.slice(url.origin.length)
113117
}
114118

115119
serve(req, res, next)
@@ -123,16 +127,17 @@ export function serveRawFsMiddleware(
123127

124128
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
125129
return function viteServeRawFsMiddleware(req, res, next) {
126-
let url = decodeURIComponent(req.url!)
130+
const url = new URL(req.url!, 'http://example.com')
127131
// In some cases (e.g. linked monorepos) files outside of root will
128132
// reference assets that are also out of served root. In such cases
129133
// the paths are rewritten to `/@fs/` prefixed paths and must be served by
130134
// searching based from fs root.
131-
if (url.startsWith(FS_PREFIX)) {
135+
if (url.pathname.startsWith(FS_PREFIX)) {
136+
const pathname = decodeURIComponent(url.pathname)
132137
// restrict files outside of `fs.allow`
133138
if (
134139
!ensureServingAccess(
135-
slash(path.resolve(fsPathFromId(url))),
140+
slash(path.resolve(fsPathFromId(pathname))),
136141
server,
137142
res,
138143
next
@@ -141,10 +146,11 @@ export function serveRawFsMiddleware(
141146
return
142147
}
143148

144-
url = url.slice(FS_PREFIX.length)
145-
if (isWindows) url = url.replace(/^[A-Z]:/i, '')
149+
let newPathname = pathname.slice(FS_PREFIX.length)
150+
if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, '')
146151

147-
req.url = encodeURIComponent(url)
152+
url.pathname = encodeURIComponent(newPathname)
153+
req.url = url.href.slice(url.origin.length)
148154
serveFromRoot(req, res, next)
149155
} else {
150156
next()

playground/fs-serve/__tests__/fs-serve.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe.runIf(isServe)('main', () => {
2121
expect(await page.textContent('.safe-fetch-status')).toBe('200')
2222
})
2323

24+
test('safe fetch with query', async () => {
25+
expect(await page.textContent('.safe-fetch-query')).toMatch('KEY=safe')
26+
expect(await page.textContent('.safe-fetch-query-status')).toBe('200')
27+
})
28+
2429
test('safe fetch with special characters', async () => {
2530
expect(
2631
await page.textContent('.safe-fetch-subdir-special-characters')
@@ -52,6 +57,11 @@ describe.runIf(isServe)('main', () => {
5257
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
5358
})
5459

60+
test('safe fs fetch', async () => {
61+
expect(await page.textContent('.safe-fs-fetch-query')).toBe(stringified)
62+
expect(await page.textContent('.safe-fs-fetch-query-status')).toBe('200')
63+
})
64+
5565
test('safe fs fetch with special characters', async () => {
5666
expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
5767
stringified

playground/fs-serve/root/src/index.html

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ <h2>Normal Import</h2>
77
<h2>Safe Fetch</h2>
88
<pre class="safe-fetch-status"></pre>
99
<pre class="safe-fetch"></pre>
10+
<pre class="safe-fetch-query-status"></pre>
11+
<pre class="safe-fetch-query"></pre>
1012

1113
<h2>Safe Fetch Subdirectory</h2>
1214
<pre class="safe-fetch-subdir-status"></pre>
@@ -25,6 +27,8 @@ <h2>Unsafe Fetch</h2>
2527
<h2>Safe /@fs/ Fetch</h2>
2628
<pre class="safe-fs-fetch-status"></pre>
2729
<pre class="safe-fs-fetch"></pre>
30+
<pre class="safe-fs-fetch-query-status"></pre>
31+
<pre class="safe-fs-fetch-query"></pre>
2832
<pre class="safe-fs-fetch-special-characters-status"></pre>
2933
<pre class="safe-fs-fetch-special-characters"></pre>
3034

@@ -58,6 +62,17 @@ <h2>Denied</h2>
5862
.then((data) => {
5963
text('.safe-fetch', JSON.stringify(data))
6064
})
65+
66+
// inside allowed dir with query, safe fetch
67+
fetch('/src/safe.txt?query')
68+
.then((r) => {
69+
text('.safe-fetch-query-status', r.status)
70+
return r.text()
71+
})
72+
.then((data) => {
73+
text('.safe-fetch-query', JSON.stringify(data))
74+
})
75+
6176
// inside allowed dir, safe fetch
6277
fetch('/src/subdir/safe.txt')
6378
.then((r) => {
@@ -127,6 +142,16 @@ <h2>Denied</h2>
127142
text('.safe-fs-fetch', JSON.stringify(data))
128143
})
129144

145+
// imported before with query, should be treated as safe
146+
fetch('/@fs/' + ROOT + '/safe.json?query')
147+
.then((r) => {
148+
text('.safe-fs-fetch-query-status', r.status)
149+
return r.json()
150+
})
151+
.then((data) => {
152+
text('.safe-fs-fetch-query', JSON.stringify(data))
153+
})
154+
130155
// not imported before, outside of root, treated as unsafe
131156
fetch('/@fs/' + ROOT + '/unsafe.json')
132157
.then((r) => {

0 commit comments

Comments
 (0)