Skip to content

Commit

Permalink
fix(serve): prevent serving unrestricted files (#3321)
Browse files Browse the repository at this point in the history
  • Loading branch information
underfin committed May 10, 2021
1 parent b93bf9c commit 7231b5a
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 42 deletions.
10 changes: 7 additions & 3 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import fs from 'fs'
import path from 'path'
import { Plugin } from './plugin'
import { BuildOptions, resolveBuildOptions } from './build'
import { ServerOptions } from './server'
import {
ResolvedServerOptions,
resolveServerOptions,
ServerOptions
} from './server'
import { CSSOptions } from './plugins/css'
import {
createDebugger,
Expand Down Expand Up @@ -201,7 +205,7 @@ export type ResolvedConfig = Readonly<
alias: Alias[]
}
plugins: readonly Plugin[]
server: ServerOptions
server: ResolvedServerOptions
build: ResolvedBuildOptions
assetsInclude: (file: string) => boolean
logger: Logger
Expand Down Expand Up @@ -392,7 +396,7 @@ export async function resolveConfig(
mode,
isProduction,
plugins: userPlugins,
server: config.server || {},
server: resolveServerOptions(resolvedRoot, config.server),
build: resolvedBuildOptions,
env: {
...userEnv,
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type {
CorsOptions,
FileSystemServeOptions,
CorsOrigin,
ServerHook
ServerHook,
ResolvedServerOptions
} from './server'
export type {
BuildOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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
Expand Down
21 changes: 19 additions & 2 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -126,6 +127,10 @@ export interface ServerOptions {
fsServe?: FileSystemServeOptions
}

export interface ResolvedServerOptions extends ServerOptions {
fsServe: Required<FileSystemServeOptions>
}

export interface FileSystemServeOptions {
/**
* Restrict accessing files outside this directory will result in a 403.
Expand Down Expand Up @@ -280,7 +285,7 @@ export async function createServer(
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server || {}
const serverConfig = config.server
const middlewareMode = !!serverConfig.middlewareMode
const httpsOptions = await resolveHttpsConfig(config)

Expand Down Expand Up @@ -555,7 +560,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') {
Expand Down Expand Up @@ -681,3 +686,15 @@ function createServerCloseFn(server: http.Server | null) {
}
})
}

export function resolveServerOptions(
root: string,
raw?: ServerOptions
): ResolvedServerOptions {
const server = raw || {}
const serverRoot = normalizePath(
path.resolve(root, server.fsServe?.root || searchForWorkspaceRoot(root))
)
server.fsServe = { root: serverRoot }
return server as ResolvedServerOptions
}
27 changes: 27 additions & 0 deletions packages/vite/src/node/server/middlewares/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,35 @@ export function errorMiddleware(
if (allowNext) {
next()
} else {
if (err instanceof AccessRestrictedError) {
res.statusCode = 403
res.write(renderErrorHTML(err.message))
res.end()
}
res.statusCode = 500
res.end()
}
}
}

export class AccessRestrictedError 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`
<body>
<h1>403 Restricted</h1>
<p>${msg.replace(/\n/g, '<br/>')}</p>
<style>
body {
padding: 1em 2em;
}
</style>
</body>
`
}
50 changes: 15 additions & 35 deletions packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { AccessRestrictedError } from './error'

const sirvOptions: Options = {
dev: true,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
ensureServingAccess(
path.resolve(fsPathFromId(url)),
config.server.fsServe.root
)

url = url.slice(FS_PREFIX.length)
if (isWin) url = url.replace(/^[A-Z]:/i, '')
Expand All @@ -112,28 +106,14 @@ export function serveRawFsMiddleware(
}
}

function renderFsRestrictedHTML(root: string) {
// to have syntax highlighting and autocompletion in IDE
const html = String.raw
return html`
<body>
<h1>403 Restricted</h1>
<p>
For security concerns, accessing files outside of workspace root
(<code>${root}</code>) is restricted since Vite v2.3.x
</p>
<p>
Refer to docs
<a href="https://vitejs.dev/config/#server-fsserveroot">
https://vitejs.dev/config/#server-fsserveroot
</a>
for configurations and more details.
</p>
<style>
body {
padding: 1em 2em;
}
</style>
</body>
`
export function ensureServingAccess(url: string, serveRoot: string): void {
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.
Refer to docs https://vitejs.dev/config/#server-fsserveroot for configurations and more details.`,
url,
serveRoot
)
}
}
4 changes: 4 additions & 0 deletions packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { checkPublicFile } from '../plugins/asset'
import { ssrTransform } from '../ssr/ssrTransform'
import { injectSourcesContent } from './sourcemap'
import { ensureServingAccess } from './middlewares/static'

const debugLoad = createDebugger('vite:load')
const debugTransform = createDebugger('vite:transform')
Expand Down Expand Up @@ -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) {
ensureServingAccess(file, config.server.fsServe.root)
}
code = await fs.readFile(file, 'utf-8')
isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)
} catch (e) {
Expand Down

0 comments on commit 7231b5a

Please sign in to comment.