Skip to content

Commit c45a02f

Browse files
antfuShinigami92
andauthored
feat: fs-serve import graph awareness (#3784)
Co-authored-by: Shinigami <[email protected]>
1 parent aada0c5 commit c45a02f

File tree

17 files changed

+235
-68
lines changed

17 files changed

+235
-68
lines changed

docs/config/index.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,12 +496,12 @@ createServer()
496496

497497
Restrict serving files outside of workspace root.
498498

499-
### server.fsServe.root
499+
### server.fsServe.allow
500500

501501
- **Experimental**
502-
- **Type:** `string`
502+
- **Type:** `string[]`
503503

504-
Restrict files that could be served via `/@fs/`. When `server.fsServe.strict` is set to `true`, accessing files outside this directory will result in a 403.
504+
Restrict files that could be served via `/@fs/`. When `server.fsServe.strict` is set to `true`, accessing files outside this directory list will result in a 403.
505505

506506
Vite will search for the root of the potential workspace and use it as default. A valid workspace met the following conditions, otherwise will fallback to the [project root](/guide/#index-html-and-project-root).
507507

@@ -516,7 +516,9 @@ createServer()
516516
server: {
517517
fsServe: {
518518
// Allow serving files from one level up to the project root
519-
root: '..'
519+
allow: [
520+
'..'
521+
]
520522
}
521523
}
522524
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { isBuild } from '../../testUtils'
2+
3+
const json = require('../safe.json')
4+
const stringified = JSON.stringify(json)
5+
6+
if (!isBuild) {
7+
test('default import', async () => {
8+
expect(await page.textContent('.full')).toBe(stringified)
9+
})
10+
11+
test('named import', async () => {
12+
expect(await page.textContent('.named')).toBe(json.msg)
13+
})
14+
15+
test('safe fetch', async () => {
16+
expect(await page.textContent('.safe-fetch')).toBe(stringified)
17+
expect(await page.textContent('.safe-fetch-status')).toBe('200')
18+
})
19+
20+
test('unsafe fetch', async () => {
21+
expect(await page.textContent('.unsafe-fetch')).toBe('')
22+
expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
23+
})
24+
25+
test('nested entry', async () => {
26+
expect(await page.textContent('.nested-entry')).toBe('foobar')
27+
})
28+
} else {
29+
test('dummy test to make jest happy', async () => {
30+
// Your test suite must contain at least one test.
31+
})
32+
}

packages/playground/fs-serve/entry.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { msg } from './nested/foo'
2+
3+
export const fullmsg = msg + 'bar'
4+
5+
document.querySelector('.nested-entry').textContent = fullmsg
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = 'foo'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "test-fs-serve",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"dev": "vite root",
7+
"build": "vite build root",
8+
"debug": "node --inspect-brk ../../vite/bin/vite",
9+
"serve": "vite preview"
10+
}
11+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<h2>Normal Import</h2>
2+
<pre class="full"></pre>
3+
<pre class="named"></pre>
4+
5+
<h2>Safe Fetch</h2>
6+
<pre class="safe-fetch-status"></pre>
7+
<pre class="safe-fetch"></pre>
8+
9+
<h2>Unsafe Fetch</h2>
10+
<pre class="unsafe-fetch-status"></pre>
11+
<pre class="unsafe-fetch"></pre>
12+
13+
<h2>Nested Entry</h2>
14+
<pre class="nested-entry"></pre>
15+
16+
<script type="module">
17+
import '../entry'
18+
import json, { msg } from '../safe.json'
19+
20+
text('.full', JSON.stringify(json))
21+
text('.named', msg)
22+
23+
// imported before, should be treated as safe
24+
fetch('/@fs/' + ROOT + '/safe.json')
25+
.then((r) => {
26+
text('.safe-fetch-status', r.status)
27+
return r.json()
28+
})
29+
.then((data) => {
30+
text('.safe-fetch', JSON.stringify(data))
31+
})
32+
33+
// not imported before, outside of root, treated as unsafe
34+
fetch('/@fs/' + ROOT + '/unsafe.json')
35+
.then((r) => {
36+
text('.unsafe-fetch-status', r.status)
37+
return r.json()
38+
})
39+
.then((data) => {
40+
text('.unsafe-fetch', JSON.stringify(data))
41+
})
42+
.catch((e) => {
43+
console.error(e)
44+
})
45+
46+
function text(sel, text) {
47+
document.querySelector(sel).textContent = text
48+
}
49+
</script>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const path = require('path')
2+
3+
/**
4+
* @type {import('vite').UserConfig}
5+
*/
6+
module.exports = {
7+
server: {
8+
fsServe: {
9+
root: __dirname,
10+
strict: true
11+
},
12+
hmr: {
13+
overlay: false
14+
}
15+
},
16+
define: {
17+
ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/'))
18+
}
19+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"msg": "safe"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"msg": "unsafe"
3+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
361361
)
362362
let url = normalizedUrl
363363

364+
// record as safe modules
365+
server?.moduleGraph.safeModulesPath.add(
366+
cleanUrl(url).slice(4 /* '/@fs'.length */)
367+
)
368+
364369
// rewrite
365370
if (url !== specifier) {
366371
// for optimized cjs deps, support named imports by rewriting named

packages/vite/src/node/server/index.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
import { timeMiddleware } from './middlewares/time'
3434
import { ModuleGraph, ModuleNode } from './moduleGraph'
3535
import { Connect } from 'types/connect'
36-
import { createDebugger, normalizePath } from '../utils'
36+
import { createDebugger, ensureLeadingSlash, normalizePath } from '../utils'
3737
import { errorMiddleware, prepareError } from './middlewares/error'
3838
import { handleHMRUpdate, HmrOptions, handleFileAddUnlink } from './hmr'
3939
import { openBrowser } from './openBrowser'
@@ -53,6 +53,7 @@ import { createMissingImporterRegisterFn } from '../optimizer/registerMissing'
5353
import { printServerUrls } from '../logger'
5454
import { resolveHostname } from '../utils'
5555
import { searchForWorkspaceRoot } from './searchRoot'
56+
import { CLIENT_DIR } from '../constants'
5657

5758
export interface ServerOptions {
5859
host?: string | boolean
@@ -129,28 +130,37 @@ export interface ServerOptions {
129130
}
130131

131132
export interface ResolvedServerOptions extends ServerOptions {
132-
fsServe: Required<FileSystemServeOptions>
133+
fsServe: Required<Omit<FileSystemServeOptions, 'root'>>
133134
}
134135

135136
export interface FileSystemServeOptions {
136137
/**
137138
* Strictly restrict file accessing outside of allowing paths.
138139
*
140+
* Set to `false` to disable the warning
139141
* Default to false at this moment, will enabled by default in the future versions.
142+
*
143+
* @expiremental
144+
* @default undefined
145+
*/
146+
strict?: boolean | undefined
147+
148+
/**
149+
*
140150
* @expiremental
141-
* @default false
151+
* @deprecated use `fsServe.allow` instead
142152
*/
143-
strict?: boolean
153+
root?: string
144154

145155
/**
146-
* Restrict accessing files outside this directory will result in a 403.
156+
* Restrict accessing files outside the allowed directories.
147157
*
148158
* Accepts absolute path or a path relative to project root.
149159
* Will try to search up for workspace root by default.
150160
*
151161
* @expiremental
152162
*/
153-
root?: string
163+
allow?: string[]
154164
}
155165

156166
/**
@@ -487,7 +497,7 @@ export async function createServer(
487497
middlewares.use(transformMiddleware(server))
488498

489499
// serve static files
490-
middlewares.use(serveRawFsMiddleware(config))
500+
middlewares.use(serveRawFsMiddleware(server))
491501
middlewares.use(serveStaticMiddleware(root, config))
492502

493503
// spa fallback
@@ -698,19 +708,32 @@ function createServerCloseFn(server: http.Server | null) {
698708
})
699709
}
700710

711+
function resolvedAllowDir(root: string, dir: string): string {
712+
return ensureLeadingSlash(normalizePath(path.resolve(root, dir)))
713+
}
714+
701715
export function resolveServerOptions(
702716
root: string,
703717
raw?: ServerOptions
704718
): ResolvedServerOptions {
705719
const server = raw || {}
706-
const fsServeRoot = normalizePath(
707-
path.resolve(root, server.fsServe?.root || searchForWorkspaceRoot(root))
708-
)
709-
// TODO: make strict by default
710-
const fsServeStrict = server.fsServe?.strict ?? false
720+
let allowDirs = server.fsServe?.allow
721+
722+
if (!allowDirs) {
723+
allowDirs = [server.fsServe?.root || searchForWorkspaceRoot(root)]
724+
}
725+
allowDirs = allowDirs.map((i) => resolvedAllowDir(root, i))
726+
727+
// only push client dir when vite itself is outside-of-root
728+
const resolvedClientDir = resolvedAllowDir(root, CLIENT_DIR)
729+
if (!allowDirs.some((i) => resolvedClientDir.startsWith(i))) {
730+
allowDirs.push(resolvedClientDir)
731+
}
732+
711733
server.fsServe = {
712-
root: fsServeRoot,
713-
strict: fsServeStrict
734+
// TODO: make strict by default
735+
strict: server.fsServe?.strict,
736+
allow: allowDirs
714737
}
715738
return server as ResolvedServerOptions
716739
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function errorMiddleware(
7575
}
7676

7777
export class AccessRestrictedError extends Error {
78-
constructor(msg: string, public url: string, public serveRoot: string) {
78+
constructor(msg: string) {
7979
super(msg)
8080
}
8181
}

0 commit comments

Comments
 (0)