Skip to content

Commit 1ab06a8

Browse files
xinxinhe1810benmccannsapphi-red
authoredJul 29, 2023
fix(importAnalysis): strip url base before passing as safeModulePaths (#13712)
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
1 parent 66f522c commit 1ab06a8

File tree

6 files changed

+204
-24
lines changed

6 files changed

+204
-24
lines changed
 

‎packages/vite/src/node/plugins/importAnalysis.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
557557
}
558558

559559
// record as safe modules
560-
server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url))
560+
// safeModulesPath should not include the base prefix.
561+
// See https://github.com/vitejs/vite/issues/9438#issuecomment-1465270409
562+
server?.moduleGraph.safeModulesPath.add(
563+
fsPathFromUrl(stripBase(url, base)),
564+
)
561565

562566
if (url !== specifier) {
563567
let rewriteDone = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import fetch from 'node-fetch'
2+
import { beforeAll, describe, expect, test } from 'vitest'
3+
import testJSON from '../../safe.json'
4+
import { isServe, page, viteTestUrl } from '~utils'
5+
6+
const stringified = JSON.stringify(testJSON)
7+
8+
describe.runIf(isServe)('main', () => {
9+
beforeAll(async () => {
10+
const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
11+
await page.goto(viteTestUrl + srcPrefix + 'src/')
12+
})
13+
14+
test('default import', async () => {
15+
expect(await page.textContent('.full')).toBe(stringified)
16+
})
17+
18+
test('named import', async () => {
19+
expect(await page.textContent('.named')).toBe(testJSON.msg)
20+
})
21+
22+
test('safe fetch', async () => {
23+
expect(await page.textContent('.safe-fetch')).toMatch('KEY=safe')
24+
expect(await page.textContent('.safe-fetch-status')).toBe('200')
25+
})
26+
27+
test('safe fetch with query', async () => {
28+
expect(await page.textContent('.safe-fetch-query')).toMatch('KEY=safe')
29+
expect(await page.textContent('.safe-fetch-query-status')).toBe('200')
30+
})
31+
32+
test('safe fetch with special characters', async () => {
33+
expect(
34+
await page.textContent('.safe-fetch-subdir-special-characters'),
35+
).toMatch('KEY=safe')
36+
expect(
37+
await page.textContent('.safe-fetch-subdir-special-characters-status'),
38+
).toBe('200')
39+
})
40+
41+
test('unsafe fetch', async () => {
42+
expect(await page.textContent('.unsafe-fetch')).toMatch('403 Restricted')
43+
expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
44+
})
45+
46+
test('unsafe fetch with special characters (#8498)', async () => {
47+
expect(await page.textContent('.unsafe-fetch-8498')).toBe('')
48+
expect(await page.textContent('.unsafe-fetch-8498-status')).toBe('404')
49+
})
50+
51+
test('unsafe fetch with special characters 2 (#8498)', async () => {
52+
expect(await page.textContent('.unsafe-fetch-8498-2')).toBe('')
53+
expect(await page.textContent('.unsafe-fetch-8498-2-status')).toBe('404')
54+
})
55+
56+
test('safe fs fetch', async () => {
57+
expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
58+
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
59+
})
60+
61+
test('safe fs fetch', async () => {
62+
expect(await page.textContent('.safe-fs-fetch-query')).toBe(stringified)
63+
expect(await page.textContent('.safe-fs-fetch-query-status')).toBe('200')
64+
})
65+
66+
test('safe fs fetch with special characters', async () => {
67+
expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
68+
stringified,
69+
)
70+
expect(
71+
await page.textContent('.safe-fs-fetch-special-characters-status'),
72+
).toBe('200')
73+
})
74+
75+
test('unsafe fs fetch', async () => {
76+
expect(await page.textContent('.unsafe-fs-fetch')).toBe('')
77+
expect(await page.textContent('.unsafe-fs-fetch-status')).toBe('403')
78+
})
79+
80+
test('unsafe fs fetch with special characters (#8498)', async () => {
81+
expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('')
82+
expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
83+
})
84+
85+
test('unsafe fs fetch with special characters 2 (#8498)', async () => {
86+
expect(await page.textContent('.unsafe-fs-fetch-8498-2')).toBe('')
87+
expect(await page.textContent('.unsafe-fs-fetch-8498-2-status')).toBe('404')
88+
})
89+
90+
test('nested entry', async () => {
91+
expect(await page.textContent('.nested-entry')).toBe('foobar')
92+
})
93+
94+
test('denied', async () => {
95+
expect(await page.textContent('.unsafe-dotenv')).toBe('404')
96+
})
97+
})
98+
99+
describe('fetch', () => {
100+
test('serve with configured headers', async () => {
101+
const res = await fetch(viteTestUrl + '/src/')
102+
expect(res.headers.get('x-served-by')).toBe('vite')
103+
})
104+
})

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

+5-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const stringified = JSON.stringify(testJSON)
77

88
describe.runIf(isServe)('main', () => {
99
beforeAll(async () => {
10-
await page.goto(viteTestUrl + '/src/')
10+
const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
11+
await page.goto(viteTestUrl + srcPrefix + 'src/')
1112
})
1213

1314
test('default import', async () => {
@@ -66,7 +67,9 @@ describe.runIf(isServe)('main', () => {
6667
expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
6768
stringified,
6869
)
69-
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
70+
expect(
71+
await page.textContent('.safe-fs-fetch-special-characters-status'),
72+
).toBe('200')
7073
})
7174

7275
test('unsafe fs fetch', async () => {
@@ -88,10 +91,6 @@ describe.runIf(isServe)('main', () => {
8891
expect(await page.textContent('.nested-entry')).toBe('foobar')
8992
})
9093

91-
test('nested entry', async () => {
92-
expect(await page.textContent('.nested-entry')).toBe('foobar')
93-
})
94-
9594
test('denied', async () => {
9695
expect(await page.textContent('.unsafe-dotenv')).toBe('404')
9796
})

‎playground/fs-serve/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"dev": "vite root",
88
"build": "vite build root",
99
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
10-
"preview": "vite preview root"
10+
"preview": "vite preview root",
11+
"dev:base": "vite root --config ./root/vite.config-base.js",
12+
"build:base": "vite build root --config ./root/vite.config-base.js",
13+
"preview:base": "vite preview root --config ./root/vite.config-base.js"
1114
}
1215
}

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

+50-16
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,26 @@ <h2>Denied</h2>
5050
import '../../entry'
5151
import json, { msg } from '../../safe.json'
5252

53+
function joinUrlSegments(a, b) {
54+
if (!a || !b) {
55+
return a || b || ''
56+
}
57+
if (a[a.length - 1] === '/') {
58+
a = a.substring(0, a.length - 1)
59+
}
60+
if (b[0] !== '/') {
61+
b = '/' + b
62+
}
63+
return a + b
64+
}
65+
5366
text('.full', JSON.stringify(json))
5467
text('.named', msg)
5568

69+
const base = typeof BASE !== 'undefined' ? BASE : ''
70+
5671
// inside allowed dir, safe fetch
57-
fetch('/src/safe.txt')
72+
fetch(joinUrlSegments(base, '/src/safe.txt'))
5873
.then((r) => {
5974
text('.safe-fetch-status', r.status)
6075
return r.text()
@@ -64,7 +79,7 @@ <h2>Denied</h2>
6479
})
6580

6681
// inside allowed dir with query, safe fetch
67-
fetch('/src/safe.txt?query')
82+
fetch(joinUrlSegments(base, '/src/safe.txt?query'))
6883
.then((r) => {
6984
text('.safe-fetch-query-status', r.status)
7085
return r.text()
@@ -74,7 +89,7 @@ <h2>Denied</h2>
7489
})
7590

7691
// inside allowed dir, safe fetch
77-
fetch('/src/subdir/safe.txt')
92+
fetch(joinUrlSegments(base, '/src/subdir/safe.txt'))
7893
.then((r) => {
7994
text('.safe-fetch-subdir-status', r.status)
8095
return r.text()
@@ -84,7 +99,12 @@ <h2>Denied</h2>
8499
})
85100

86101
// inside allowed dir, with special characters, safe fetch
87-
fetch('/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.txt')
102+
fetch(
103+
joinUrlSegments(
104+
base,
105+
'/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.txt',
106+
),
107+
)
88108
.then((r) => {
89109
text('.safe-fetch-subdir-special-characters-status', r.status)
90110
return r.text()
@@ -94,7 +114,7 @@ <h2>Denied</h2>
94114
})
95115

96116
// outside of allowed dir, treated as unsafe
97-
fetch('/unsafe.txt')
117+
fetch(joinUrlSegments(base, '/unsafe.txt'))
98118
.then((r) => {
99119
text('.unsafe-fetch-status', r.status)
100120
return r.text()
@@ -107,7 +127,7 @@ <h2>Denied</h2>
107127
})
108128

109129
// outside of allowed dir with special characters #8498
110-
fetch('/src/%2e%2e%2funsafe%2etxt')
130+
fetch(joinUrlSegments(base, '/src/%2e%2e%2funsafe%2etxt'))
111131
.then((r) => {
112132
text('.unsafe-fetch-8498-status', r.status)
113133
return r.text()
@@ -120,7 +140,7 @@ <h2>Denied</h2>
120140
})
121141

122142
// outside of allowed dir with special characters 2 #8498
123-
fetch('/src/%252e%252e%252funsafe%252etxt')
143+
fetch(joinUrlSegments(base, '/src/%252e%252e%252funsafe%252etxt'))
124144
.then((r) => {
125145
text('.unsafe-fetch-8498-2-status', r.status)
126146
return r.text()
@@ -133,7 +153,7 @@ <h2>Denied</h2>
133153
})
134154

135155
// imported before, should be treated as safe
136-
fetch('/@fs/' + ROOT + '/safe.json')
156+
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/safe.json'))
137157
.then((r) => {
138158
text('.safe-fs-fetch-status', r.status)
139159
return r.json()
@@ -143,7 +163,9 @@ <h2>Denied</h2>
143163
})
144164

145165
// imported before with query, should be treated as safe
146-
fetch('/@fs/' + ROOT + '/safe.json?query')
166+
fetch(
167+
joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/safe.json?query'),
168+
)
147169
.then((r) => {
148170
text('.safe-fs-fetch-query-status', r.status)
149171
return r.json()
@@ -153,7 +175,7 @@ <h2>Denied</h2>
153175
})
154176

155177
// not imported before, outside of root, treated as unsafe
156-
fetch('/@fs/' + ROOT + '/unsafe.json')
178+
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/unsafe.json'))
157179
.then((r) => {
158180
text('.unsafe-fs-fetch-status', r.status)
159181
return r.json()
@@ -166,7 +188,13 @@ <h2>Denied</h2>
166188
})
167189

168190
// outside root with special characters #8498
169-
fetch('/@fs/' + ROOT + '/root/src/%2e%2e%2f%2e%2e%2funsafe%2ejson')
191+
fetch(
192+
joinUrlSegments(
193+
base,
194+
joinUrlSegments('/@fs/', ROOT) +
195+
'/root/src/%2e%2e%2f%2e%2e%2funsafe%2ejson',
196+
),
197+
)
170198
.then((r) => {
171199
text('.unsafe-fs-fetch-8498-status', r.status)
172200
return r.json()
@@ -177,7 +205,11 @@ <h2>Denied</h2>
177205

178206
// outside root with special characters 2 #8498
179207
fetch(
180-
'/@fs/' + ROOT + '/root/src/%252e%252e%252f%252e%252e%252funsafe%252ejson',
208+
joinUrlSegments(
209+
base,
210+
joinUrlSegments('/@fs/', ROOT) +
211+
'/root/src/%252e%252e%252f%252e%252e%252funsafe%252ejson',
212+
),
181213
)
182214
.then((r) => {
183215
text('.unsafe-fs-fetch-8498-2-status', r.status)
@@ -189,9 +221,11 @@ <h2>Denied</h2>
189221

190222
// not imported before, inside root with special characters, treated as safe
191223
fetch(
192-
'/@fs/' +
193-
ROOT +
194-
'/root/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.json',
224+
joinUrlSegments(
225+
base,
226+
joinUrlSegments('/@fs/', ROOT) +
227+
'/root/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.json',
228+
),
195229
)
196230
.then((r) => {
197231
text('.safe-fs-fetch-special-characters-status', r.status)
@@ -202,7 +236,7 @@ <h2>Denied</h2>
202236
})
203237

204238
// .env, denied by default
205-
fetch('/@fs/' + ROOT + '/root/.env')
239+
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/root/.env'))
206240
.then((r) => {
207241
text('.unsafe-dotenv', r.status)
208242
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import path from 'node:path'
2+
import { defineConfig } from 'vite'
3+
4+
const BASE = '/base/'
5+
6+
export default defineConfig({
7+
base: BASE,
8+
build: {
9+
rollupOptions: {
10+
input: {
11+
main: path.resolve(__dirname, 'src/index.html'),
12+
},
13+
},
14+
},
15+
server: {
16+
fs: {
17+
strict: true,
18+
allow: [path.resolve(__dirname, 'src')],
19+
},
20+
hmr: {
21+
overlay: false,
22+
},
23+
headers: {
24+
'x-served-by': 'vite',
25+
},
26+
},
27+
preview: {
28+
headers: {
29+
'x-served-by': 'vite',
30+
},
31+
},
32+
define: {
33+
ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
34+
BASE: JSON.stringify(BASE),
35+
},
36+
})

0 commit comments

Comments
 (0)
Please sign in to comment.