diff --git a/packages/playground/fs-serve/root/src/index.html b/packages/playground/fs-serve/root/src/index.html
index c8b294e86ab0ea..9e4f728a593a91 100644
--- a/packages/playground/fs-serve/root/src/index.html
+++ b/packages/playground/fs-serve/root/src/index.html
@@ -8,6 +8,10 @@
Safe Fetch
+Safe Fetch Subdirectory
+
+
+
Unsafe Fetch
@@ -42,6 +46,15 @@ Denied
.then((data) => {
text('.safe-fetch', JSON.stringify(data))
})
+ // inside allowed dir, safe fetch
+ fetch('/src/subdir/safe.txt')
+ .then((r) => {
+ text('.safe-fetch-subdir-status', r.status)
+ return r.text()
+ })
+ .then((data) => {
+ text('.safe-fetch-subdir', JSON.stringify(data))
+ })
// outside of allowed dir, treated as unsafe
fetch('/unsafe.txt')
diff --git a/packages/playground/fs-serve/root/src/subdir/safe.txt b/packages/playground/fs-serve/root/src/subdir/safe.txt
new file mode 100644
index 00000000000000..3f3d0607101642
--- /dev/null
+++ b/packages/playground/fs-serve/root/src/subdir/safe.txt
@@ -0,0 +1 @@
+KEY=safe
diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts
index 953304e0ac38c2..082aa35dfae213 100644
--- a/packages/vite/src/node/plugins/importAnalysis.ts
+++ b/packages/vite/src/node/plugins/importAnalysis.ts
@@ -21,7 +21,8 @@ import {
normalizePath,
removeImportQuery,
unwrapId,
- moduleListContains
+ moduleListContains,
+ fsPathFromUrl
} from '../utils'
import {
debugHmr,
@@ -399,9 +400,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
let url = normalizedUrl
// record as safe modules
- server?.moduleGraph.safeModulesPath.add(
- cleanUrl(url).slice(4 /* '/@fs'.length */)
- )
+ server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url))
// rewrite
if (url !== specifier) {
diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts
index aeea862dbc173f..8dd1f371e989b8 100644
--- a/packages/vite/src/node/server/index.ts
+++ b/packages/vite/src/node/server/index.ts
@@ -33,7 +33,7 @@ import { timeMiddleware } from './middlewares/time'
import type { ModuleNode } from './moduleGraph'
import { ModuleGraph } from './moduleGraph'
import type { Connect } from 'types/connect'
-import { ensureLeadingSlash, normalizePath } from '../utils'
+import { isParentDirectory, normalizePath } from '../utils'
import { errorMiddleware, prepareError } from './middlewares/error'
import type { HmrOptions } from './hmr'
import { handleHMRUpdate, handleFileAddUnlink } from './hmr'
@@ -693,7 +693,7 @@ function createServerCloseFn(server: http.Server | null) {
}
function resolvedAllowDir(root: string, dir: string): string {
- return ensureLeadingSlash(normalizePath(path.resolve(root, dir)))
+ return normalizePath(path.resolve(root, dir))
}
export function resolveServerOptions(
@@ -715,7 +715,7 @@ export function resolveServerOptions(
// only push client dir when vite itself is outside-of-root
const resolvedClientDir = resolvedAllowDir(root, CLIENT_DIR)
- if (!allowDirs.some((i) => resolvedClientDir.startsWith(i))) {
+ if (!allowDirs.some((dir) => isParentDirectory(dir, resolvedClientDir))) {
allowDirs.push(resolvedClientDir)
}
diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts
index 0b8e2db93255ed..a6623338783cc8 100644
--- a/packages/vite/src/node/server/middlewares/static.ts
+++ b/packages/vite/src/node/server/middlewares/static.ts
@@ -4,17 +4,17 @@ import type { Options } from 'sirv'
import sirv from 'sirv'
import type { Connect } from 'types/connect'
import type { ViteDevServer } from '../..'
-import { normalizePath } from '../..'
import { FS_PREFIX } from '../../constants'
import {
cleanUrl,
- ensureLeadingSlash,
fsPathFromId,
+ fsPathFromUrl,
isImportRequest,
isInternalRequest,
isWindows,
slash,
- isFileReadable
+ isFileReadable,
+ isParentDirectory
} from '../../utils'
import { isMatch } from 'micromatch'
@@ -148,15 +148,14 @@ export function isFileServingAllowed(
): boolean {
if (!server.config.server.fs.strict) return true
- const cleanedUrl = cleanUrl(url)
- const file = ensureLeadingSlash(normalizePath(cleanedUrl))
+ const file = fsPathFromUrl(url)
if (server.config.server.fs.deny.some((i) => isMatch(file, i, _matchOptions)))
return false
if (server.moduleGraph.safeModulesPath.has(file)) return true
- if (server.config.server.fs.allow.some((i) => file.startsWith(i + '/')))
+ if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file)))
return true
return false
diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts
index d723b25a54122d..cade91cc24306d 100644
--- a/packages/vite/src/node/utils.ts
+++ b/packages/vite/src/node/utils.ts
@@ -9,7 +9,8 @@ import {
DEFAULT_EXTENSIONS,
VALID_ID_PREFIX,
CLIENT_PUBLIC_PATH,
- ENV_PUBLIC_PATH
+ ENV_PUBLIC_PATH,
+ CLIENT_ENTRY
} from './constants'
import resolve from 'resolve'
import { builtinModules } from 'module'
@@ -139,7 +140,25 @@ export function createDebugger(
}
}
+function testCaseInsensitiveFS() {
+ if (!CLIENT_ENTRY.endsWith('client.mjs')) {
+ throw new Error(
+ `cannot test case insensitive FS, CLIENT_ENTRY const doesn't contain client.mjs`
+ )
+ }
+ if (!fs.existsSync(CLIENT_ENTRY)) {
+ throw new Error(
+ 'cannot test case insensitive FS, CLIENT_ENTRY does not point to an existing file: ' +
+ CLIENT_ENTRY
+ )
+ }
+ return fs.existsSync(CLIENT_ENTRY.replace('client.mjs', 'cLiEnT.mjs'))
+}
+
+export const isCaseInsensitiveFS = testCaseInsensitiveFS()
+
export const isWindows = os.platform() === 'win32'
+
const VOLUME_RE = /^[A-Z]:/i
export function normalizePath(id: string): string {
@@ -147,12 +166,37 @@ export function normalizePath(id: string): string {
}
export function fsPathFromId(id: string): string {
- const fsPath = normalizePath(id.slice(FS_PREFIX.length))
+ const fsPath = normalizePath(
+ id.startsWith(FS_PREFIX) ? id.slice(FS_PREFIX.length) : id
+ )
return fsPath.startsWith('/') || fsPath.match(VOLUME_RE)
? fsPath
: `/${fsPath}`
}
+export function fsPathFromUrl(url: string): string {
+ return fsPathFromId(cleanUrl(url))
+}
+
+/**
+ * Check if dir is a parent of file
+ *
+ * Warning: parameters are not validated, only works with normalized absolute paths
+ *
+ * @param dir - normalized absolute path
+ * @param file - normalized absolute path
+ * @returns true if dir is a parent of file
+ */
+export function isParentDirectory(dir: string, file: string): boolean {
+ if (!dir.endsWith('/')) {
+ dir = `${dir}/`
+ }
+ return (
+ file.startsWith(dir) ||
+ (isCaseInsensitiveFS && file.toLowerCase().startsWith(dir.toLowerCase()))
+ )
+}
+
export function ensureVolumeInPath(file: string): string {
return isWindows ? path.resolve(file) : file
}
@@ -466,10 +510,6 @@ export function copyDir(srcDir: string, destDir: string): void {
}
}
-export function ensureLeadingSlash(path: string): string {
- return !path.startsWith('/') ? '/' + path : path
-}
-
export function ensureWatchedFile(
watcher: FSWatcher,
file: string | null,