Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guide/api-environment-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ function createWorkerdDevEnvironment(
}
```

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.

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.

## `ModuleRunner`
Expand Down Expand Up @@ -323,6 +325,8 @@ function createWorkerEnvironment(name, config, context) {
}

const workerHotChannel = {
// Worker threads post messages are not exposed over the network, skip server.fs checks
skipFsCheck: true,
send: (data) => worker.postMessage(data),
on: (event, handler) => {
// client is already connected
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ function defaultCreateClientDevEnvironment(
return new DevEnvironment(name, config, {
hot: true,
transport: context.ws,
disableFetchModule: true,
})
}

Expand Down
29 changes: 18 additions & 11 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import { EnvironmentModuleGraph } from './moduleGraph'
import type { EnvironmentModuleNode } from './moduleGraph'
import type { HotChannel, NormalizedHotChannel } from './hmr'
import { getShortName, normalizeHotChannel, updateModules } from './hmr'
import type {
TransformOptionsInternal,
TransformResult,
} from './transformRequest'
import type { TransformResult } from './transformRequest'
import { transformRequest } from './transformRequest'
import type { EnvironmentPluginContainer } from './pluginContainer'
import {
Expand All @@ -44,6 +41,8 @@ export interface DevEnvironmentContext {
inlineSourceMap?: boolean
}
depsOptimizer?: DepsOptimizer
/** @internal used for client environment */
disableFetchModule?: boolean
}

export class DevEnvironment extends BaseEnvironment {
Expand All @@ -55,6 +54,10 @@ export class DevEnvironment extends BaseEnvironment {
* @internal
*/
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
/**
* @internal
*/
_skipFsCheck: boolean

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

this._remoteRunnerOptions = context.remoteRunner ?? {}
this._skipFsCheck = !!(
context.transport &&
!(isWebSocketServer in context.transport) &&
context.transport.skipFsCheck
)

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

this.hot.setInvokeHandler({
fetchModule: (id, importer, options) => {
if (context.disableFetchModule) {
throw new Error('fetchModule is disabled in this environment')
}
return this.fetchModule(id, importer, options)
},
getBuiltins: async () => {
Expand Down Expand Up @@ -216,17 +227,13 @@ export class DevEnvironment extends BaseEnvironment {
}
}

transformRequest(
url: string,
/** @internal */
options?: TransformOptionsInternal,
): Promise<TransformResult | null> {
return transformRequest(this, url, options)
transformRequest(url: string): Promise<TransformResult | null> {
return transformRequest(this, url, { skipFsCheck: this._skipFsCheck })
}

async warmupRequest(url: string): Promise<void> {
try {
await this.transformRequest(url)
await transformRequest(this, url, { skipFsCheck: true })
} catch (e) {
if (
e?.code === ERR_OUTDATED_OPTIMIZED_DEP ||
Expand Down
6 changes: 6 additions & 0 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export type HotChannelListener<T extends string = string> = (
) => void

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

return {
skipFsCheck: true,
send(payload: HotPayload) {
outsideEmitter.emit('send', payload)
},
Expand Down
14 changes: 5 additions & 9 deletions packages/vite/src/node/server/middlewares/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const rawRE = /[?&]raw\b/
const inlineRE = /[?&]inline\b/
const svgRE = /\.svg\b/

function isServerAccessDeniedForTransform(config: ResolvedConfig, id: string) {
export function isServerAccessDeniedForTransform(
config: ResolvedConfig,
id: string,
): boolean {
if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) {
return checkLoadingAccess(config, id) !== 'allowed'
}
Expand Down Expand Up @@ -244,14 +247,7 @@ export function transformMiddleware(
}

// resolve, load and transform using the plugin container
const result = await environment.transformRequest(url, {
allowId(id) {
return (
id[0] === '\0' ||
!isServerAccessDeniedForTransform(server.config, id)
)
},
})
const result = await environment.transformRequest(url)
if (result) {
const depsOptimizer = environment.depsOptimizer
const type = isDirectCSSRequest(url) ? 'css' : 'js'
Expand Down
17 changes: 11 additions & 6 deletions packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { isFileLoadingAllowed } from './middlewares/static'
import { throwClosedServerError } from './pluginContainer'
import type { DevEnvironment } from './environment'
import { isServerAccessDeniedForTransform } from './middlewares/transform'

export const ERR_LOAD_URL = 'ERR_LOAD_URL'
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
Expand All @@ -55,11 +56,11 @@ export interface TransformOptions {
ssr?: boolean
}

export interface TransformOptionsInternal {
interface TransformOptionsInternal {
/**
* @internal
* Whether to skip the `server.fs` check.
*/
allowId?: (id: string) => boolean
skipFsCheck: boolean
}

// TODO: This function could be moved to the DevEnvironment class.
Expand All @@ -72,7 +73,7 @@ export interface TransformOptionsInternal {
export function transformRequest(
environment: DevEnvironment,
url: string,
options: TransformOptionsInternal = {},
options: TransformOptionsInternal,
): Promise<TransformResult | null> {
if (environment._closing && environment.config.dev.recoverable)
throwClosedServerError()
Expand Down Expand Up @@ -243,7 +244,11 @@ async function loadAndTransform(

const moduleGraph = environment.moduleGraph

if (options.allowId && !options.allowId(id)) {
if (
!options.skipFsCheck &&
id[0] !== '\0' &&
isServerAccessDeniedForTransform(config, id)
) {
const err: any = new Error(`Denied ID ${id}`)
err.code = ERR_DENIED_ID
err.id = id
Expand All @@ -266,7 +271,7 @@ async function loadAndTransform(
// only try the fallback if access is allowed, skip for out of root url
// like /service-worker.js or /api/users
if (
environment.config.consumer === 'server' ||
options.skipFsCheck ||
isFileLoadingAllowed(environment.getTopLevelConfig(), slash(file))
) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'ok'
23 changes: 23 additions & 0 deletions packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,26 @@ test('buildStart before transform', async () => {
]
`)
})

test('server.fs check is not applied to ssrLoadModule', async () => {
const server = await createServer({
configFile: false,
root,
logLevel: 'silent',
optimizeDeps: {
noDiscovery: true,
},
server: {
fs: {
allow: [
path.resolve(import.meta.dirname, './fixtures/named-overwrite-all'),
],
},
},
})
onTestFinished(() => server.close())
await server.environments.ssr.pluginContainer.buildStart({})

const mod = await server.ssrLoadModule('/fixtures/basic/file.js')
expect(mod.default).toBe('ok')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'error'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, readdirSync } from 'node:fs'
import { posix, win32 } from 'node:path'
import { posix, resolve, win32 } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, vi } from 'vitest'
import { isWindows } from '../../../../shared/utils'
Expand Down Expand Up @@ -515,3 +515,18 @@ describe('virtual module hmr', async () => {
}
})
})

describe('server.fs check', async () => {
const it = await createModuleRunnerTester({
server: {
fs: {
allow: [resolve(import.meta.dirname, './fixtures/circular')],
},
},
})

it('it is not applied to the server module runner', async ({ runner }) => {
const mod = await runner.import('/fixtures/basic.js')
expect(mod.name).toBe('basic')
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BroadcastChannel, Worker } from 'node:worker_threads'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { BirpcReturn } from 'birpc'
import { createBirpc } from 'birpc'
Expand Down Expand Up @@ -34,6 +35,9 @@ describe('running module runner inside a worker and using the ModuleRunnerTransp
hmr: {
port: 9610,
},
fs: {
allow: [path.resolve(import.meta.dirname, './fixtures')],
},
},
environments: {
worker: {
Expand Down Expand Up @@ -107,4 +111,13 @@ describe('running module runner inside a worker and using the ModuleRunnerTransp
expect(output.result).toBe('baz.txt')
expect(output.error).toBeUndefined()
})

it('server.fs check is applied to the custom transport by default', async () => {
handleInvoke = (data: any) =>
server.environments.worker.hot.handleInvoke(data)

const output = await run('./fixture-outside.js')
expect(output).toHaveProperty('error')
expect(output.error).toContain('Failed to load url')
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BroadcastChannel, Worker } from 'node:worker_threads'
import path from 'node:path'
import { describe, expect, it, onTestFinished } from 'vitest'
import type { HotChannel, HotChannelListener, HotPayload } from 'vite'
import { DevEnvironment } from '../../..'
Expand Down Expand Up @@ -112,4 +113,48 @@ describe('running module runner inside a worker', () => {
channel.postMessage({ id: './fixtures/default-string.ts' })
})
})

it('server.fs check is applied to the custom transport by default', async () => {
const worker = new Worker(
new URL('./fixtures/worker.mjs', import.meta.url),
{ stdout: true },
)
await new Promise<void>((resolve, reject) => {
worker.on('message', () => resolve())
worker.on('error', reject)
})
const server = await createServer({
root: import.meta.dirname,
logLevel: 'error',
server: {
middlewareMode: true,
watch: null,
hmr: {
port: 9609,
},
fs: {
allow: [path.resolve(import.meta.dirname, './fixtures')],
},
},
environments: {
worker: {
dev: {
createEnvironment: (name, config) => {
return new DevEnvironment(name, config, {
hot: false,
transport: createWorkerTransport(worker),
})
},
},
},
},
})
onTestFinished(async () => {
await Promise.allSettled([server.close(), worker.terminate()])
})

await expect(
server.environments.worker.transformRequest('./fixture-outside.js'),
).rejects.toThrow('Failed to load url')
})
})
Loading
Loading