Skip to content

Commit fe28e47

Browse files
authored
fix: apply server.fs check to env transport (#22159) (#22163)
1 parent 6e37ac2 commit fe28e47

15 files changed

Lines changed: 244 additions & 18 deletions

File tree

docs/guide/api-environment-runtimes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ function createWorkerEnvironment(name, config, context) {
304304
const handlerToWorkerListener = new WeakMap()
305305

306306
const workerHotChannel = {
307+
// Worker threads post messages are not exposed over the network, skip server.fs checks
308+
skipFsCheck: true,
307309
send: (data) => worker.postMessage(data),
308310
on: (event, handler) => {
309311
if (event === 'connection') return

packages/vite/src/node/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ function defaultCreateClientDevEnvironment(
219219
return new DevEnvironment(name, config, {
220220
hot: true,
221221
transport: context.ws,
222+
disableFetchModule: true,
222223
})
223224
}
224225

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
21
import type { FSWatcher } from 'dep-types/chokidar'
32
import colors from 'picocolors'
3+
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
44
import {
55
BaseEnvironment,
66
getDefaultResolvedEnvironmentOptions,
@@ -45,6 +45,8 @@ export interface DevEnvironmentContext {
4545
inlineSourceMap?: boolean
4646
}
4747
depsOptimizer?: DepsOptimizer
48+
/** @internal used for client environment */
49+
disableFetchModule?: boolean
4850
}
4951

5052
export class DevEnvironment extends BaseEnvironment {
@@ -56,6 +58,10 @@ export class DevEnvironment extends BaseEnvironment {
5658
* @internal
5759
*/
5860
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
61+
/**
62+
* @internal
63+
*/
64+
_skipFsCheck: boolean
5965

6066
get pluginContainer(): EnvironmentPluginContainer {
6167
if (!this._pluginContainer)
@@ -121,6 +127,11 @@ export class DevEnvironment extends BaseEnvironment {
121127
this._crawlEndFinder = setupOnCrawlEnd()
122128

123129
this._remoteRunnerOptions = context.remoteRunner ?? {}
130+
this._skipFsCheck = !!(
131+
context.transport &&
132+
!(isWebSocketServer in context.transport) &&
133+
context.transport.skipFsCheck
134+
)
124135

125136
this.hot = context.transport
126137
? isWebSocketServer in context.transport
@@ -130,6 +141,9 @@ export class DevEnvironment extends BaseEnvironment {
130141

131142
this.hot.setInvokeHandler({
132143
fetchModule: (id, importer, options) => {
144+
if (context.disableFetchModule) {
145+
throw new Error('fetchModule is disabled in this environment')
146+
}
133147
return this.fetchModule(id, importer, options)
134148
},
135149
})
@@ -210,12 +224,12 @@ export class DevEnvironment extends BaseEnvironment {
210224
}
211225

212226
transformRequest(url: string): Promise<TransformResult | null> {
213-
return transformRequest(this, url)
227+
return transformRequest(this, url, { skipFsCheck: this._skipFsCheck })
214228
}
215229

216230
async warmupRequest(url: string): Promise<void> {
217231
try {
218-
await this.transformRequest(url)
232+
await transformRequest(this, url, { skipFsCheck: true })
219233
} catch (e) {
220234
if (
221235
e?.code === ERR_OUTDATED_OPTIMIZED_DEP ||

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ export type HotChannelListener<T extends string = string> = (
8787
) => void
8888

8989
export interface HotChannel<Api = any> {
90+
/**
91+
* When true, the fs access check is skipped in fetchModule.
92+
* Set this for transports that is not exposed over the network.
93+
*/
94+
skipFsCheck?: boolean
9095
/**
9196
* Broadcast events to all clients
9297
*/
@@ -1132,6 +1137,7 @@ export function createServerHotChannel(): ServerHotChannel {
11321137
const outsideEmitter = new EventEmitter()
11331138

11341139
return {
1140+
skipFsCheck: true,
11351141
send(payload: HotPayload) {
11361142
outsideEmitter.emit('send', payload)
11371143
},

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ import {
4141
ERR_OUTDATED_OPTIMIZED_DEP,
4242
NULL_BYTE_PLACEHOLDER,
4343
} from '../../../shared/constants'
44-
import { checkServingAccess, respondWithAccessDenied } from './static'
44+
import type { ResolvedConfig } from '../../config'
45+
import {
46+
checkLoadingAccess,
47+
checkServingAccess,
48+
respondWithAccessDenied,
49+
} from './static'
4550

4651
const debugCache = createDebugger('vite:cache')
4752

@@ -80,6 +85,16 @@ function deniedServingAccessForTransform(
8085
return false
8186
}
8287

88+
export function isServerAccessDeniedForTransform(
89+
config: ResolvedConfig,
90+
id: string,
91+
): boolean {
92+
if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) {
93+
return checkLoadingAccess(config, id) !== 'allowed'
94+
}
95+
return false
96+
}
97+
8398
/**
8499
* A middleware that short-circuits the middleware chain to serve cached transformed modules
85100
*/
@@ -266,9 +281,7 @@ export function transformMiddleware(
266281
// resolve, load and transform using the plugin container
267282
const result = await transformRequest(environment, url, {
268283
html: req.headers.accept?.includes('text/html'),
269-
allowId(id) {
270-
return !deniedServingAccessForTransform(id, server, res, next)
271-
},
284+
skipFsCheck: environment._skipFsCheck,
272285
})
273286
if (result) {
274287
const depsOptimizer = environment.depsOptimizer
@@ -340,8 +353,24 @@ export function transformMiddleware(
340353
return next()
341354
}
342355
if (e?.code === ERR_DENIED_ID) {
343-
// next() is called in ensureServingAccess
344-
return
356+
const id: string = e.id
357+
let servingAccessResult = checkLoadingAccess(
358+
server.config,
359+
cleanUrl(id),
360+
)
361+
if (servingAccessResult === 'allowed') {
362+
servingAccessResult = checkLoadingAccess(server.config, id)
363+
}
364+
if (servingAccessResult === 'denied') {
365+
respondWithAccessDenied(id, server, res)
366+
return true
367+
}
368+
if (servingAccessResult === 'fallback') {
369+
next()
370+
return true
371+
}
372+
servingAccessResult satisfies 'allowed'
373+
throw new Error(`Unexpected access result for id ${id}`)
345374
}
346375
return next(e)
347376
}

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '../utils'
2121
import { ssrTransform } from '../ssr/ssrTransform'
2222
import { checkPublicFile } from '../publicDir'
23-
import { cleanUrl, unwrapId } from '../../shared/utils'
23+
import { cleanUrl, slash, unwrapId } from '../../shared/utils'
2424
import {
2525
applySourcemapIgnoreList,
2626
extractSourcemapFromFile,
@@ -29,6 +29,7 @@ import {
2929
import { isFileLoadingAllowed } from './middlewares/static'
3030
import { throwClosedServerError } from './pluginContainer'
3131
import type { DevEnvironment } from './environment'
32+
import { isServerAccessDeniedForTransform } from './middlewares/transform'
3233

3334
export const ERR_LOAD_URL = 'ERR_LOAD_URL'
3435
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
@@ -57,9 +58,10 @@ export interface TransformOptions {
5758
*/
5859
html?: boolean
5960
/**
61+
* Whether to skip the `server.fs` check.
6062
* @internal
6163
*/
62-
allowId?: (id: string) => boolean
64+
skipFsCheck?: boolean
6365
}
6466

6567
// TODO: This function could be moved to the DevEnvironment class.
@@ -253,8 +255,13 @@ async function loadAndTransform(
253255

254256
const moduleGraph = environment.moduleGraph
255257

256-
if (options.allowId && !options.allowId(id)) {
258+
if (
259+
!options.skipFsCheck &&
260+
id[0] !== '\0' &&
261+
isServerAccessDeniedForTransform(config, id)
262+
) {
257263
const err: any = new Error(`Denied ID ${id}`)
264+
err.id = id
258265
err.code = ERR_DENIED_ID
259266
throw err
260267
}
@@ -280,8 +287,8 @@ async function loadAndTransform(
280287
// only try the fallback if access is allowed, skip for out of root url
281288
// like /service-worker.js or /api/users
282289
if (
283-
environment.config.consumer === 'server' ||
284-
isFileLoadingAllowed(environment.getTopLevelConfig(), file)
290+
options.skipFsCheck ||
291+
isFileLoadingAllowed(environment.getTopLevelConfig(), slash(file))
285292
) {
286293
try {
287294
code = await fsp.readFile(file, 'utf-8')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'ok'

packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,26 @@ test('buildStart before transform', async () => {
376376
]
377377
`)
378378
})
379+
380+
test('server.fs check is not applied to ssrLoadModule', async () => {
381+
const server = await createServer({
382+
configFile: false,
383+
root,
384+
logLevel: 'silent',
385+
optimizeDeps: {
386+
noDiscovery: true,
387+
},
388+
server: {
389+
fs: {
390+
allow: [
391+
path.resolve(import.meta.dirname, './fixtures/named-overwrite-all'),
392+
],
393+
},
394+
},
395+
})
396+
onTestFinished(() => server.close())
397+
await server.environments.ssr.pluginContainer.buildStart({})
398+
399+
const mod = await server.ssrLoadModule('/fixtures/basic/file.js')
400+
expect(mod.default).toBe('ok')
401+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'error'

packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync, readdirSync } from 'node:fs'
2-
import { posix, win32 } from 'node:path'
2+
import { posix, resolve, win32 } from 'node:path'
33
import { fileURLToPath } from 'node:url'
44
import { describe, expect, vi } from 'vitest'
55
import { isWindows } from '../../../../shared/utils'
@@ -481,3 +481,18 @@ describe('virtual module hmr', async () => {
481481
})
482482
})
483483
})
484+
485+
describe('server.fs check', async () => {
486+
const it = await createModuleRunnerTester({
487+
server: {
488+
fs: {
489+
allow: [resolve(import.meta.dirname, './fixtures/circular')],
490+
},
491+
},
492+
})
493+
494+
it('it is not applied to the server module runner', async ({ runner }) => {
495+
const mod = await runner.import('/fixtures/basic.js')
496+
expect(mod.name).toBe('basic')
497+
})
498+
})

0 commit comments

Comments
 (0)