From 9ca1dd16dbd71f3316a4d4126fddbc62c2e521fe Mon Sep 17 00:00:00 2001 From: underfin Date: Sun, 9 May 2021 19:49:58 +0800 Subject: [PATCH 1/7] fix(serve): prevent serving unrestricted files fix: #3281 --- packages/vite/src/node/config.ts | 14 ++++-- packages/vite/src/node/server/index.ts | 8 +++- .../vite/src/node/server/middlewares/error.ts | 27 +++++++++++ .../src/node/server/middlewares/static.ts | 48 +++++-------------- .../vite/src/node/server/transformRequest.ts | 4 ++ 5 files changed, 61 insertions(+), 40 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 5581cb59c26812..a9fb8ff581972a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'path' import { Plugin } from './plugin' import { BuildOptions, resolveBuildOptions } from './build' -import { ServerOptions } from './server' +import { ResolvedServerOptions, ServerOptions } from './server' import { CSSOptions } from './plugins/css' import { createDebugger, @@ -35,6 +35,7 @@ import { } from './server/pluginContainer' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' +import { searchForWorkspaceRoot } from './server/searchRoot' const debug = createDebugger('vite:config') @@ -201,7 +202,7 @@ export type ResolvedConfig = Readonly< alias: Alias[] } plugins: readonly Plugin[] - server: ServerOptions + server: ResolvedServerOptions build: ResolvedBuildOptions assetsInclude: (file: string) => boolean logger: Logger @@ -369,6 +370,13 @@ export async function resolveConfig( } } + const server = config.server || {} + const serverRoot = path.resolve( + resolvedRoot, + server.fsServe?.root || searchForWorkspaceRoot(resolvedRoot) + ) + server.fsServe = { root: serverRoot } + const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' @@ -392,7 +400,7 @@ export async function resolveConfig( mode, isProduction, plugins: userPlugins, - server: config.server || {}, + server: server as ResolvedServerOptions, build: resolvedBuildOptions, env: { ...userEnv, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 97db4d2f5ab827..ae5a5eeb8a7399 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -126,6 +126,10 @@ export interface ServerOptions { fsServe?: FileSystemServeOptions } +export interface ResolvedServerOptions extends ServerOptions { + fsServe: Required +} + export interface FileSystemServeOptions { /** * Restrict accessing files outside this directory will result in a 403. @@ -276,7 +280,7 @@ export async function createServer( ): Promise { const config = await resolveConfig(inlineConfig, 'serve', 'development') const root = config.root - const serverConfig = config.server || {} + const serverConfig = config.server const middlewareMode = !!serverConfig.middlewareMode const middlewares = connect() as Connect.Server @@ -550,7 +554,7 @@ async function startServer( throw new Error('Cannot call server.listen in middleware mode.') } - const options = server.config.server || {} + const options = server.config.server let port = inlinePort || options.port || 3000 let hostname: string | undefined if (options.host === undefined || options.host === 'localhost') { diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index 7433637d341757..dea8d7b51d7ad4 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -63,8 +63,35 @@ export function errorMiddleware( if (allowNext) { next() } else { + if (err instanceof FileOutSideError) { + res.statusCode = 403 + res.write(renderErrorHTML(err.message)) + res.end() + } res.statusCode = 500 res.end() } } } + +export class FileOutSideError extends Error { + constructor(msg: string, public url: string, public serveRoot: string) { + super(msg) + } +} + +export function renderErrorHTML(msg: string): string { + // to have syntax highlighting and autocompletion in IDE + const html = String.raw + return html` + +

403 Restricted

+

${msg.replace(/\n/g, '
')}

+ + + ` +} diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index f6d47fa71e7ad0..f782fcea13154f 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -5,7 +5,7 @@ import { Connect } from 'types/connect' import { ResolvedConfig } from '../..' import { FS_PREFIX } from '../../constants' import { cleanUrl, fsPathFromId, isImportRequest } from '../../utils' -import { searchForWorkspaceRoot } from '../searchRoot' +import { FileOutSideError } from './error' const sirvOptions: Options = { dev: true, @@ -80,10 +80,6 @@ export function serveRawFsMiddleware( ): Connect.NextHandleFunction { const isWin = os.platform() === 'win32' const serveFromRoot = sirv('/', sirvOptions) - const serveRoot = path.resolve( - config.root, - config.server?.fsServe?.root || searchForWorkspaceRoot(config.root) - ) // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteServeRawFsMiddleware(req, res, next) { @@ -94,12 +90,10 @@ export function serveRawFsMiddleware( // searching based from fs root. if (url.startsWith(FS_PREFIX)) { // restrict files outside of `fsServe.root` - if (!path.resolve(fsPathFromId(url)).startsWith(serveRoot + path.sep)) { - res.statusCode = 403 - res.write(renderFsRestrictedHTML(serveRoot)) - res.end() - return - } + checkFileOutSide( + path.resolve(fsPathFromId(url)), + config.server.fsServe.root + ) url = url.slice(FS_PREFIX.length) if (isWin) url = url.replace(/^[A-Z]:/i, '') @@ -112,28 +106,12 @@ export function serveRawFsMiddleware( } } -function renderFsRestrictedHTML(root: string) { - // to have syntax highlighting and autocompletion in IDE - const html = String.raw - return html` - -

403 Restricted

-

- For security concerns, accessing files outside of workspace root - (${root}) is restricted since Vite v2.3.x -

-

- Refer to docs - - https://vitejs.dev/config/#server-fsserveroot - - for configurations and more details. -

- - - ` +export function checkFileOutSide(url: string, serveRoot: string): void { + if (!url.startsWith(serveRoot + path.sep)) { + throw new FileOutSideError( + `The request url "${url}" is outside of vite dev server root "${serveRoot}". \nFor security concerns, accessing files outside of workspace root is restricted since Vite v2.3.x. \nRefer to docs https://vitejs.dev/config/#server-fsserveroot for configurations and more details.`, + url, + serveRoot + ) + } } diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 8907ec6c173635..0765879233a940 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -16,6 +16,7 @@ import { import { checkPublicFile } from '../plugins/asset' import { ssrTransform } from '../ssr/ssrTransform' import { injectSourcesContent } from './sourcemap' +import { checkFileOutSide } from './middlewares/static' const debugLoad = createDebugger('vite:load') const debugTransform = createDebugger('vite:transform') @@ -73,6 +74,9 @@ export async function transformRequest( // if the file is a binary, there should be a plugin that already loaded it // as string try { + if (!options.ssr) { + checkFileOutSide(file, config.server.fsServe.root) + } code = await fs.readFile(file, 'utf-8') isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`) } catch (e) { From dea02885c5709e8dbd0b8ae3b41b6e6b0968ee81 Mon Sep 17 00:00:00 2001 From: underfin Date: Sun, 9 May 2021 20:22:17 +0800 Subject: [PATCH 2/7] fix: fix build --- packages/vite/src/node/index.ts | 3 ++- packages/vite/src/node/preview.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 6199988279e2c4..47206fe69ca744 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -14,7 +14,8 @@ export type { CorsOptions, FileSystemServeOptions, CorsOrigin, - ServerHook + ServerHook, + ResolvedServerOptions } from './server' export type { BuildOptions, diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 9663084fd78bb4..ffb6a54034347e 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -40,7 +40,7 @@ export async function preview( }) ) - const options = config.server || {} + const options = config.server let hostname: string | undefined if (options.host === undefined || options.host === 'localhost') { // Use a secure default From e3344fd01d5e351e33231499d1143c4497396156 Mon Sep 17 00:00:00 2001 From: underfin Date: Sun, 9 May 2021 22:06:55 +0800 Subject: [PATCH 3/7] fix: fix test --- packages/vite/src/node/config.ts | 2 +- packages/vite/src/node/server/middlewares/static.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a9fb8ff581972a..141030137d21f3 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -371,7 +371,7 @@ export async function resolveConfig( } const server = config.server || {} - const serverRoot = path.resolve( + const serverRoot = path.posix.resolve( resolvedRoot, server.fsServe?.root || searchForWorkspaceRoot(resolvedRoot) ) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index f782fcea13154f..ff9168889adc4f 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -109,7 +109,9 @@ export function serveRawFsMiddleware( export function checkFileOutSide(url: string, serveRoot: string): void { if (!url.startsWith(serveRoot + path.sep)) { throw new FileOutSideError( - `The request url "${url}" is outside of vite dev server root "${serveRoot}". \nFor security concerns, accessing files outside of workspace root is restricted since Vite v2.3.x. \nRefer to docs https://vitejs.dev/config/#server-fsserveroot for configurations and more details.`, + `The request url "${url}" is outside of vite dev server root "${serveRoot}". + For security concerns, accessing files outside of workspace root is restricted since Vite v2.3.x. + Refer to docs https://vitejs.dev/config/#server-fsserveroot for configurations and more details.`, url, serveRoot ) From b571db3154b724a3ff8042df9cf5c59d5b421984 Mon Sep 17 00:00:00 2001 From: underfin Date: Mon, 10 May 2021 13:10:46 +0800 Subject: [PATCH 4/7] fix: commit review changes --- packages/vite/src/node/config.ts | 15 ++++++--------- packages/vite/src/node/server/index.ts | 14 ++++++++++++++ .../vite/src/node/server/middlewares/error.ts | 4 ++-- .../vite/src/node/server/middlewares/static.ts | 8 ++++---- packages/vite/src/node/server/transformRequest.ts | 4 ++-- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 141030137d21f3..cbb1770e43ca91 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -2,7 +2,11 @@ import fs from 'fs' import path from 'path' import { Plugin } from './plugin' import { BuildOptions, resolveBuildOptions } from './build' -import { ResolvedServerOptions, ServerOptions } from './server' +import { + ResolvedServerOptions, + resolveServerOptions, + ServerOptions +} from './server' import { CSSOptions } from './plugins/css' import { createDebugger, @@ -370,13 +374,6 @@ export async function resolveConfig( } } - const server = config.server || {} - const serverRoot = path.posix.resolve( - resolvedRoot, - server.fsServe?.root || searchForWorkspaceRoot(resolvedRoot) - ) - server.fsServe = { root: serverRoot } - const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' @@ -400,7 +397,7 @@ export async function resolveConfig( mode, isProduction, plugins: userPlugins, - server: server as ResolvedServerOptions, + server: resolveServerOptions(resolvedRoot, config.server), build: resolvedBuildOptions, env: { ...userEnv, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index ae5a5eeb8a7399..a06086bf2af99f 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -51,6 +51,7 @@ import { resolveSSRExternal } from '../ssr/ssrExternal' import { ssrRewriteStacktrace } from '../ssr/ssrStacktrace' import { createMissingImporterRegisterFn } from '../optimizer/registerMissing' import { printServerUrls } from '../logger' +import { searchForWorkspaceRoot } from './searchRoot' export interface ServerOptions { host?: string | boolean @@ -680,3 +681,16 @@ function createServerCloseFn(server: http.Server | null) { } }) } + +export function resolveServerOptions( + root: string, + raw?: ServerOptions +): ResolvedServerOptions { + const server = raw || {} + const serverRoot = path.resolve( + root, + server.fsServe?.root || searchForWorkspaceRoot(root) + ) + server.fsServe = { root: serverRoot } + return server as ResolvedServerOptions +} diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index dea8d7b51d7ad4..3bc6eca98b2f71 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -63,7 +63,7 @@ export function errorMiddleware( if (allowNext) { next() } else { - if (err instanceof FileOutSideError) { + if (err instanceof AccessRestrictedError) { res.statusCode = 403 res.write(renderErrorHTML(err.message)) res.end() @@ -74,7 +74,7 @@ export function errorMiddleware( } } -export class FileOutSideError extends Error { +export class AccessRestrictedError extends Error { constructor(msg: string, public url: string, public serveRoot: string) { super(msg) } diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index ff9168889adc4f..156cb7c3b6d06c 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -5,7 +5,7 @@ import { Connect } from 'types/connect' import { ResolvedConfig } from '../..' import { FS_PREFIX } from '../../constants' import { cleanUrl, fsPathFromId, isImportRequest } from '../../utils' -import { FileOutSideError } from './error' +import { AccessRestrictedError } from './error' const sirvOptions: Options = { dev: true, @@ -90,7 +90,7 @@ export function serveRawFsMiddleware( // searching based from fs root. if (url.startsWith(FS_PREFIX)) { // restrict files outside of `fsServe.root` - checkFileOutSide( + ensureServingAccess( path.resolve(fsPathFromId(url)), config.server.fsServe.root ) @@ -106,9 +106,9 @@ export function serveRawFsMiddleware( } } -export function checkFileOutSide(url: string, serveRoot: string): void { +export function ensureServingAccess(url: string, serveRoot: string): void { if (!url.startsWith(serveRoot + path.sep)) { - throw new FileOutSideError( + throw new AccessRestrictedError( `The request url "${url}" is outside of vite dev server root "${serveRoot}". For security concerns, accessing files outside of workspace root is restricted since Vite v2.3.x. Refer to docs https://vitejs.dev/config/#server-fsserveroot for configurations and more details.`, diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 0765879233a940..7aa784d700f1e2 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -16,7 +16,7 @@ import { import { checkPublicFile } from '../plugins/asset' import { ssrTransform } from '../ssr/ssrTransform' import { injectSourcesContent } from './sourcemap' -import { checkFileOutSide } from './middlewares/static' +import { ensureServingAccess } from './middlewares/static' const debugLoad = createDebugger('vite:load') const debugTransform = createDebugger('vite:transform') @@ -75,7 +75,7 @@ export async function transformRequest( // as string try { if (!options.ssr) { - checkFileOutSide(file, config.server.fsServe.root) + ensureServingAccess(file, config.server.fsServe.root) } code = await fs.readFile(file, 'utf-8') isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`) From 42be6b41bcd9beef8754d20f65d91c15189a1c81 Mon Sep 17 00:00:00 2001 From: underfin Date: Mon, 10 May 2021 13:13:08 +0800 Subject: [PATCH 5/7] fix: remove unused import --- packages/vite/src/node/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index cbb1770e43ca91..f6bf7c08dcfb64 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -39,7 +39,6 @@ import { } from './server/pluginContainer' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' -import { searchForWorkspaceRoot } from './server/searchRoot' const debug = createDebugger('vite:config') From f20fe0ec5b5cb94dc5890f2fb22c430c335ea1e1 Mon Sep 17 00:00:00 2001 From: underfin Date: Mon, 10 May 2021 13:23:57 +0800 Subject: [PATCH 6/7] fix: try fix window ci --- packages/vite/src/node/server/middlewares/static.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index 156cb7c3b6d06c..2a9a0db1918a91 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -107,7 +107,7 @@ export function serveRawFsMiddleware( } export function ensureServingAccess(url: string, serveRoot: string): void { - if (!url.startsWith(serveRoot + path.sep)) { + if (!url.startsWith(serveRoot + path.posix.sep)) { throw new AccessRestrictedError( `The request url "${url}" is outside of vite dev server root "${serveRoot}". For security concerns, accessing files outside of workspace root is restricted since Vite v2.3.x. From b70e21cc565995a68a0ffe7c08de6304fdd93470 Mon Sep 17 00:00:00 2001 From: underfin Date: Mon, 10 May 2021 19:06:09 +0800 Subject: [PATCH 7/7] fix: fix window ci --- packages/vite/src/node/server/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a06086bf2af99f..2935957d50cce7 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -687,9 +687,8 @@ export function resolveServerOptions( raw?: ServerOptions ): ResolvedServerOptions { const server = raw || {} - const serverRoot = path.resolve( - root, - server.fsServe?.root || searchForWorkspaceRoot(root) + const serverRoot = normalizePath( + path.resolve(root, server.fsServe?.root || searchForWorkspaceRoot(root)) ) server.fsServe = { root: serverRoot } return server as ResolvedServerOptions