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 f80e471028794f..bd3161717405df 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' @@ -702,7 +702,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( @@ -724,7 +724,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 935c5c8401071b..fb479a511abbd4 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 } @@ -480,10 +524,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,