Skip to content

Commit f02d9fd

Browse files
authored
fix: apply server.fs check to env transport (#22159)
1 parent f05f501 commit f02d9fd

13 files changed

Lines changed: 213 additions & 27 deletions

File tree

docs/guide/api-environment-runtimes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ function createWorkerdDevEnvironment(
156156
}
157157
```
158158

159+
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.
160+
159161
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.
160162

161163
## `ModuleRunner`
@@ -369,6 +371,8 @@ function createWorkerEnvironment(name, config, context) {
369371
}
370372

371373
const workerHotChannel = {
374+
// Worker threads post messages are not exposed over the network, skip server.fs checks
375+
skipFsCheck: true,
372376
send: (data) => worker.postMessage(data),
373377
on: (event, handler) => {
374378
// client is already connected

packages/vite/src/node/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ function defaultCreateClientDevEnvironment(
254254
return new DevEnvironment(name, config, {
255255
hot: true,
256256
transport: context.ws,
257+
disableFetchModule: true,
257258
})
258259
}
259260

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ import type {
2626
NormalizedHotChannelClient,
2727
} from './hmr'
2828
import { getShortName, normalizeHotChannel, updateModules } from './hmr'
29-
import type {
30-
TransformOptionsInternal,
31-
TransformResult,
32-
} from './transformRequest'
29+
import type { TransformResult } from './transformRequest'
3330
import { transformRequest } from './transformRequest'
3431
import type { EnvironmentPluginContainer } from './pluginContainer'
3532
import {
@@ -48,6 +45,8 @@ export interface DevEnvironmentContext {
4845
inlineSourceMap?: boolean
4946
}
5047
depsOptimizer?: DepsOptimizer
48+
/** @internal used for client environment */
49+
disableFetchModule?: boolean
5150
/** @internal used for full bundle mode */
5251
disableDepsOptimizer?: boolean
5352
}
@@ -61,6 +60,10 @@ export class DevEnvironment extends BaseEnvironment {
6160
* @internal
6261
*/
6362
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
63+
/**
64+
* @internal
65+
*/
66+
_skipFsCheck: boolean
6467

6568
get pluginContainer(): EnvironmentPluginContainer<DevEnvironment> {
6669
if (!this._pluginContainer)
@@ -128,6 +131,11 @@ export class DevEnvironment extends BaseEnvironment {
128131
this._crawlEndFinder = setupOnCrawlEnd()
129132

130133
this._remoteRunnerOptions = context.remoteRunner ?? {}
134+
this._skipFsCheck = !!(
135+
context.transport &&
136+
!(isWebSocketServer in context.transport) &&
137+
context.transport.skipFsCheck
138+
)
131139

132140
this.hot = context.transport
133141
? isWebSocketServer in context.transport
@@ -137,6 +145,9 @@ export class DevEnvironment extends BaseEnvironment {
137145

138146
this.hot.setInvokeHandler({
139147
fetchModule: (id, importer, options) => {
148+
if (context.disableFetchModule) {
149+
throw new Error('fetchModule is disabled in this environment')
150+
}
140151
return this.fetchModule(id, importer, options)
141152
},
142153
getBuiltins: async () => {
@@ -233,17 +244,13 @@ export class DevEnvironment extends BaseEnvironment {
233244
}
234245
}
235246

236-
transformRequest(
237-
url: string,
238-
/** @internal */
239-
options?: TransformOptionsInternal,
240-
): Promise<TransformResult | null> {
241-
return transformRequest(this, url, options)
247+
transformRequest(url: string): Promise<TransformResult | null> {
248+
return transformRequest(this, url, { skipFsCheck: this._skipFsCheck })
242249
}
243250

244251
async warmupRequest(url: string): Promise<void> {
245252
try {
246-
await this.transformRequest(url)
253+
await transformRequest(this, url, { skipFsCheck: true })
247254
} catch (e) {
248255
if (
249256
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
@@ -89,6 +89,11 @@ export type HotChannelListener<T extends string = string> = (
8989
) => void
9090

9191
export interface HotChannel<Api = any> {
92+
/**
93+
* When true, the fs access check is skipped in fetchModule.
94+
* Set this for transports that is not exposed over the network.
95+
*/
96+
skipFsCheck?: boolean
9297
/**
9398
* Broadcast events to all clients
9499
*/
@@ -1130,6 +1135,7 @@ export function createServerHotChannel(): ServerHotChannel {
11301135
const outsideEmitter = new EventEmitter()
11311136

11321137
return {
1138+
skipFsCheck: true,
11331139
send(payload: HotPayload) {
11341140
outsideEmitter.emit('send', payload)
11351141
},

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
@@ -35,6 +35,7 @@ import {
3535
import { isFileLoadingAllowed } from './middlewares/static'
3636
import { throwClosedServerError } from './pluginContainer'
3737
import type { DevEnvironment } from './environment'
38+
import { isServerAccessDeniedForTransform } from './middlewares/transform'
3839

3940
export const ERR_LOAD_URL = 'ERR_LOAD_URL'
4041
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
@@ -60,11 +61,11 @@ export interface TransformOptions {
6061
ssr?: boolean
6162
}
6263

63-
export interface TransformOptionsInternal {
64+
interface TransformOptionsInternal {
6465
/**
65-
* @internal
66+
* Whether to skip the `server.fs` check.
6667
*/
67-
allowId?: (id: string) => boolean
68+
skipFsCheck: boolean
6869
}
6970

7071
// TODO: This function could be moved to the DevEnvironment class.
@@ -77,7 +78,7 @@ export interface TransformOptionsInternal {
7778
export function transformRequest(
7879
environment: DevEnvironment,
7980
url: string,
80-
options: TransformOptionsInternal = {},
81+
options: TransformOptionsInternal,
8182
): Promise<TransformResult | null> {
8283
if (environment._closing && environment.config.dev.recoverable)
8384
throwClosedServerError()
@@ -248,7 +249,11 @@ async function loadAndTransform(
248249

249250
const moduleGraph = environment.moduleGraph
250251

251-
if (options.allowId && !options.allowId(id)) {
252+
if (
253+
!options.skipFsCheck &&
254+
id[0] !== '\0' &&
255+
isServerAccessDeniedForTransform(config, id)
256+
) {
252257
const err: any = new Error(`Denied ID ${id}`)
253258
err.code = ERR_DENIED_ID
254259
err.id = id
@@ -272,7 +277,7 @@ async function loadAndTransform(
272277
// only try the fallback if access is allowed, skip for out of root url
273278
// like /service-worker.js or /api/users
274279
if (
275-
environment.config.consumer === 'server' ||
280+
options.skipFsCheck ||
276281
isFileLoadingAllowed(environment.getTopLevelConfig(), slash(file))
277282
) {
278283
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
@@ -406,3 +406,26 @@ test('buildStart before transform', async () => {
406406
]
407407
`)
408408
})
409+
410+
test('server.fs check is not applied to ssrLoadModule', async () => {
411+
const server = await createServer({
412+
configFile: false,
413+
root,
414+
logLevel: 'silent',
415+
optimizeDeps: {
416+
noDiscovery: true,
417+
},
418+
server: {
419+
fs: {
420+
allow: [
421+
path.resolve(import.meta.dirname, './fixtures/named-overwrite-all'),
422+
],
423+
},
424+
},
425+
})
426+
onTestFinished(() => server.close())
427+
await server.environments.ssr.pluginContainer.buildStart({})
428+
429+
const mod = await server.ssrLoadModule('/fixtures/basic/file.js')
430+
expect(mod.default).toBe('ok')
431+
})
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 { setTimeout } from 'node:timers/promises'
55
import { describe, expect, it, vi } from 'vitest'
@@ -625,3 +625,18 @@ describe('full-reload during close', () => {
625625
).toBe(false)
626626
})
627627
})
628+
629+
describe('server.fs check', async () => {
630+
const it = await createModuleRunnerTester({
631+
server: {
632+
fs: {
633+
allow: [resolve(import.meta.dirname, './fixtures/circular')],
634+
},
635+
},
636+
})
637+
638+
it('it is not applied to the server module runner', async ({ runner }) => {
639+
const mod = await runner.import('/fixtures/basic.js')
640+
expect(mod.name).toBe('basic')
641+
})
642+
})

0 commit comments

Comments
 (0)