Skip to content

Commit 19db0f2

Browse files
authored
fix: backport #22159, apply server.fs check to env transport (#22162)
1 parent 8409d74 commit 19db0f2

File tree

13 files changed

+213
-27
lines changed

13 files changed

+213
-27
lines changed

docs/guide/api-environment-runtimes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ function createWorkerdDevEnvironment(
110110
}
111111
```
112112

113+
By default, `HotChannel` transports have `server.fs` restrictions applied, meaning only files within the allowed directories can be served. If your transport is not exposed over the network (e.g., it communicates via worker threads or in-process calls), you can set `skipFsCheck: true` on the `HotChannel` to bypass these restrictions.
114+
113115
There are [multiple communication levels for the `DevEnvironment`](/guide/api-environment-frameworks#devenvironment-communication-levels). To make it easier for frameworks to write runtime agnostic code, we recommend to implement the most flexible communication level possible.
114116

115117
## `ModuleRunner`
@@ -323,6 +325,8 @@ function createWorkerEnvironment(name, config, context) {
323325
}
324326

325327
const workerHotChannel = {
328+
// Worker threads post messages are not exposed over the network, skip server.fs checks
329+
skipFsCheck: true,
326330
send: (data) => worker.postMessage(data),
327331
on: (event, handler) => {
328332
// client is already connected

packages/vite/src/node/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ function defaultCreateClientDevEnvironment(
231231
return new DevEnvironment(name, config, {
232232
hot: true,
233233
transport: context.ws,
234+
disableFetchModule: true,
234235
})
235236
}
236237

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import { EnvironmentModuleGraph } from './moduleGraph'
2222
import type { EnvironmentModuleNode } from './moduleGraph'
2323
import type { HotChannel, NormalizedHotChannel } from './hmr'
2424
import { getShortName, normalizeHotChannel, updateModules } from './hmr'
25-
import type {
26-
TransformOptionsInternal,
27-
TransformResult,
28-
} from './transformRequest'
25+
import type { TransformResult } from './transformRequest'
2926
import { transformRequest } from './transformRequest'
3027
import type { EnvironmentPluginContainer } from './pluginContainer'
3128
import {
@@ -44,6 +41,8 @@ export interface DevEnvironmentContext {
4441
inlineSourceMap?: boolean
4542
}
4643
depsOptimizer?: DepsOptimizer
44+
/** @internal used for client environment */
45+
disableFetchModule?: boolean
4746
}
4847

4948
export class DevEnvironment extends BaseEnvironment {
@@ -55,6 +54,10 @@ export class DevEnvironment extends BaseEnvironment {
5554
* @internal
5655
*/
5756
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
57+
/**
58+
* @internal
59+
*/
60+
_skipFsCheck: boolean
5861

5962
get pluginContainer(): EnvironmentPluginContainer<DevEnvironment> {
6063
if (!this._pluginContainer)
@@ -122,6 +125,11 @@ export class DevEnvironment extends BaseEnvironment {
122125
this._crawlEndFinder = setupOnCrawlEnd()
123126

124127
this._remoteRunnerOptions = context.remoteRunner ?? {}
128+
this._skipFsCheck = !!(
129+
context.transport &&
130+
!(isWebSocketServer in context.transport) &&
131+
context.transport.skipFsCheck
132+
)
125133

126134
this.hot = context.transport
127135
? isWebSocketServer in context.transport
@@ -131,6 +139,9 @@ export class DevEnvironment extends BaseEnvironment {
131139

132140
this.hot.setInvokeHandler({
133141
fetchModule: (id, importer, options) => {
142+
if (context.disableFetchModule) {
143+
throw new Error('fetchModule is disabled in this environment')
144+
}
134145
return this.fetchModule(id, importer, options)
135146
},
136147
getBuiltins: async () => {
@@ -216,17 +227,13 @@ export class DevEnvironment extends BaseEnvironment {
216227
}
217228
}
218229

219-
transformRequest(
220-
url: string,
221-
/** @internal */
222-
options?: TransformOptionsInternal,
223-
): Promise<TransformResult | null> {
224-
return transformRequest(this, url, options)
230+
transformRequest(url: string): Promise<TransformResult | null> {
231+
return transformRequest(this, url, { skipFsCheck: this._skipFsCheck })
225232
}
226233

227234
async warmupRequest(url: string): Promise<void> {
228235
try {
229-
await this.transformRequest(url)
236+
await transformRequest(this, url, { skipFsCheck: true })
230237
} catch (e) {
231238
if (
232239
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
@@ -84,6 +84,11 @@ export type HotChannelListener<T extends string = string> = (
8484
) => void
8585

8686
export interface HotChannel<Api = any> {
87+
/**
88+
* When true, the fs access check is skipped in fetchModule.
89+
* Set this for transports that is not exposed over the network.
90+
*/
91+
skipFsCheck?: boolean
8792
/**
8893
* Broadcast events to all clients
8994
*/
@@ -1118,6 +1123,7 @@ export function createServerHotChannel(): ServerHotChannel {
11181123
const outsideEmitter = new EventEmitter()
11191124

11201125
return {
1126+
skipFsCheck: true,
11211127
send(payload: HotPayload) {
11221128
outsideEmitter.emit('send', payload)
11231129
},

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ const rawRE = /[?&]raw\b/
5757
const inlineRE = /[?&]inline\b/
5858
const svgRE = /\.svg\b/
5959

60-
function isServerAccessDeniedForTransform(config: ResolvedConfig, id: string) {
60+
export function isServerAccessDeniedForTransform(
61+
config: ResolvedConfig,
62+
id: string,
63+
): boolean {
6164
if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) {
6265
return checkLoadingAccess(config, id) !== 'allowed'
6366
}
@@ -244,14 +247,7 @@ export function transformMiddleware(
244247
}
245248

246249
// resolve, load and transform using the plugin container
247-
const result = await environment.transformRequest(url, {
248-
allowId(id) {
249-
return (
250-
id[0] === '\0' ||
251-
!isServerAccessDeniedForTransform(server.config, id)
252-
)
253-
},
254-
})
250+
const result = await environment.transformRequest(url)
255251
if (result) {
256252
const depsOptimizer = environment.depsOptimizer
257253
const type = isDirectCSSRequest(url) ? 'css' : 'js'

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import { isFileLoadingAllowed } from './middlewares/static'
3131
import { throwClosedServerError } from './pluginContainer'
3232
import type { DevEnvironment } from './environment'
33+
import { isServerAccessDeniedForTransform } from './middlewares/transform'
3334

3435
export const ERR_LOAD_URL = 'ERR_LOAD_URL'
3536
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
@@ -55,11 +56,11 @@ export interface TransformOptions {
5556
ssr?: boolean
5657
}
5758

58-
export interface TransformOptionsInternal {
59+
interface TransformOptionsInternal {
5960
/**
60-
* @internal
61+
* Whether to skip the `server.fs` check.
6162
*/
62-
allowId?: (id: string) => boolean
63+
skipFsCheck: boolean
6364
}
6465

6566
// TODO: This function could be moved to the DevEnvironment class.
@@ -72,7 +73,7 @@ export interface TransformOptionsInternal {
7273
export function transformRequest(
7374
environment: DevEnvironment,
7475
url: string,
75-
options: TransformOptionsInternal = {},
76+
options: TransformOptionsInternal,
7677
): Promise<TransformResult | null> {
7778
if (environment._closing && environment.config.dev.recoverable)
7879
throwClosedServerError()
@@ -243,7 +244,11 @@ async function loadAndTransform(
243244

244245
const moduleGraph = environment.moduleGraph
245246

246-
if (options.allowId && !options.allowId(id)) {
247+
if (
248+
!options.skipFsCheck &&
249+
id[0] !== '\0' &&
250+
isServerAccessDeniedForTransform(config, id)
251+
) {
247252
const err: any = new Error(`Denied ID ${id}`)
248253
err.code = ERR_DENIED_ID
249254
err.id = id
@@ -266,7 +271,7 @@ async function loadAndTransform(
266271
// only try the fallback if access is allowed, skip for out of root url
267272
// like /service-worker.js or /api/users
268273
if (
269-
environment.config.consumer === 'server' ||
274+
options.skipFsCheck ||
270275
isFileLoadingAllowed(environment.getTopLevelConfig(), slash(file))
271276
) {
272277
try {
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'
@@ -515,3 +515,18 @@ describe('virtual module hmr', async () => {
515515
}
516516
})
517517
})
518+
519+
describe('server.fs check', async () => {
520+
const it = await createModuleRunnerTester({
521+
server: {
522+
fs: {
523+
allow: [resolve(import.meta.dirname, './fixtures/circular')],
524+
},
525+
},
526+
})
527+
528+
it('it is not applied to the server module runner', async ({ runner }) => {
529+
const mod = await runner.import('/fixtures/basic.js')
530+
expect(mod.name).toBe('basic')
531+
})
532+
})

0 commit comments

Comments
 (0)