diff --git a/packages/vite/src/node/__tests__/dev.spec.ts b/packages/vite/src/node/__tests__/dev.spec.ts index e0809e1c8cc7e7..0f0411f7485c40 100644 --- a/packages/vite/src/node/__tests__/dev.spec.ts +++ b/packages/vite/src/node/__tests__/dev.spec.ts @@ -122,6 +122,35 @@ describe('the dev server', () => { expect(server.config.safeModulePaths).not.toContain(externalEntry) }) + test('does not mark unresolved SSR imports as safe', async () => { + const root = path.join(import.meta.dirname, 'fixtures', 'input-option') + const entry = normalizePath(path.join(root, 'unresolved-entry.js')) + const unresolvedImport = '/safe-module-path-unresolved' + + server = await createServer({ + configFile: false, + root, + optimizeDeps: { noDiscovery: true }, + server: { middlewareMode: true, ws: false }, + plugins: [ + { + name: 'unresolved-ssr-import', + resolveId(id) { + if (id === '/unresolved-entry.js') return entry + }, + load(id) { + if (id === entry) + return `import ${JSON.stringify(unresolvedImport)}` + }, + }, + ], + }) + + await server.environments.ssr.transformRequest('/unresolved-entry.js') + + expect(server.config.safeModulePaths).not.toContain(unresolvedImport) + }) + test('silently resolves index.html as the fallback for every environment', async () => { const root = path.join(import.meta.dirname, 'fixtures', 'input-option') const clientEntry = normalizePath(path.join(root, 'client-index.html')) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index fb0c34236a0055..c6adff0f3a18a2 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -46,7 +46,6 @@ import { import type { TransformPluginContext } from '../server/pluginContainer' import { createDebugger, - fsPathFromUrl, generateCodeFrame, getFileStartIndex, getHash, @@ -587,13 +586,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize let [url, resolvedId] = await normalizeUrl(specifier, start) + if (resolvedId) { + config.safeModulePaths.add(cleanUrl(resolvedId)) + } resolvedId = resolvedId || url - // record as safe modules - // safeModulesPath should not include the base prefix. - // See https://github.com/vitejs/vite/issues/9438#issuecomment-1465270409 - config.safeModulePaths.add(fsPathFromUrl(stripBase(url, base))) - if (url !== specifier) { let rewriteDone = false // optimizer-emitted imports resolve to the sibling file they diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 42d1996513d585..e0118d8e479873 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -1,9 +1,17 @@ import net from 'node:net' import path from 'node:path' import { describe, expect, test } from 'vitest' -import { isBundledDev, isServe, isWindows, viteTestUrl } from '~utils' +import { isBundledDev, isServe, isWindows, page, viteTestUrl } from '~utils' import './commonTests' +describe.runIf(isServe)('safe module paths', () => { + test('keeps root-relative module URLs separate from absolute filesystem paths', async () => { + await expect + .poll(() => page.textContent('.safe-module-path-status')) + .toBe(isBundledDev || isWindows ? '404' : '403') + }) +}) + describe.runIf(isServe)('invalid request', () => { const sendRawRequest = async (baseUrl: string, requestTarget: string) => { return new Promise((resolve, reject) => { diff --git a/playground/fs-serve/root/safeModulePathPlugin.ts b/playground/fs-serve/root/safeModulePathPlugin.ts new file mode 100644 index 00000000000000..36b93faaf84e34 --- /dev/null +++ b/playground/fs-serve/root/safeModulePathPlugin.ts @@ -0,0 +1,24 @@ +import path from 'node:path' +import type { Plugin } from 'vite' + +const playgroundRoot = path.dirname(import.meta.dirname).replace(/\\/g, '/') +const targetRoot = playgroundRoot.replace(/^[A-Z]:/i, '') +const target = path.posix.join(targetRoot, 'root/unsafe.txt') +const decoyId = path.posix.join(import.meta.dirname.replace(/\\/g, '/'), target) + +export default function safeModulePathPlugin(): Plugin { + return { + name: 'safe-module-path', + enforce: 'pre', + resolveId(id) { + if (id === 'virtual:safe-module-path-confusion') { + return decoyId + } + }, + load(id) { + if (id === decoyId) { + return 'export {}' + } + }, + } +} diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html index f3af4bcbb46a05..b21801e08a897b 100644 --- a/playground/fs-serve/root/src/index.html +++ b/playground/fs-serve/root/src/index.html @@ -9,11 +9,17 @@

Normal Import


 
 
+
+

Safe Module Path Access Isolation

+

+
+