Skip to content

Commit

Permalink
fix: prevent serving unrestricted files (fix #2820) (#2850)
Browse files Browse the repository at this point in the history
Co-authored-by: Shinigami <chrissi92@hotmail.de>
Co-authored-by: patak <matias.capeletto@gmail.com>
  • Loading branch information
3 people committed May 7, 2021
1 parent fd4146b commit 792a6e1
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 3 deletions.
11 changes: 11 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ export default async ({ command, mode }) => {

File system watcher options to pass on to [chokidar](https://github.com/paulmillr/chokidar#api).

### server.fsServe.root

- **Type:** `string`

Restrict files that could be served via `/@fs/`. Accessing files outside this directory will result in a 403.

Vite will search for the root of the potential workspace and use it as default. A valid workspace met the following conditions, otherwise will fallback to the [project root](/guide/#index-html-and-project-root).
- contains `workspaces` field in `package.json`
- contains one of the following file
- `pnpm-workspace.yaml`

## Build Options

### build.target
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
ViteDevServer,
ServerOptions,
CorsOptions,
FileSystemServeOptions,
CorsOrigin,
ServerHook
} from './server'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"private": true,
"workspaces": [
"nested"
]
}
31 changes: 31 additions & 0 deletions packages/vite/src/node/server/__tests__/search-root.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { searchForWorkspaceRoot } from '../searchRoot'
import { resolve } from 'path'

describe('searchForWorkspaceRoot', () => {
test('pnpm', () => {
const resolved = searchForWorkspaceRoot(
resolve(__dirname, 'fixtures/pnpm/nested')
)
expect(resolved).toBe(resolve(__dirname, 'fixtures/pnpm'))
})

test('yarn', () => {
const resolved = searchForWorkspaceRoot(
resolve(__dirname, 'fixtures/yarn/nested')
)
expect(resolved).toBe(resolve(__dirname, 'fixtures/yarn'))
})

test('yarn at root', () => {
const resolved = searchForWorkspaceRoot(resolve(__dirname, 'fixtures/yarn'))
expect(resolved).toBe(resolve(__dirname, 'fixtures/yarn'))
})

test('none', () => {
const resolved = searchForWorkspaceRoot(
resolve(__dirname, 'fixtures/none/nested')
)
// resolved to vite repo's root
expect(resolved).toBe(resolve(__dirname, '../../../../../..'))
})
})
16 changes: 15 additions & 1 deletion packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ export interface ServerOptions {
* Should start and end with the `/` character
*/
base?: string
/**
* Options for files served via '/\@fs/'.
*/
fsServe?: FileSystemServeOptions
}

export interface FileSystemServeOptions {
/**
* Restrict accessing files outside this directory will result in a 403.
*
* Accepts absolute path or a path relative to project root.
* Will try to search up for workspace root by default.
*/
root?: string
}

/**
Expand Down Expand Up @@ -443,7 +457,7 @@ export async function createServer(
middlewares.use(transformMiddleware(server))

// serve static files
middlewares.use(serveRawFsMiddleware())
middlewares.use(serveRawFsMiddleware(config))
middlewares.use(serveStaticMiddleware(root, config))

// spa fallback
Expand Down
45 changes: 43 additions & 2 deletions packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import sirv, { Options } from 'sirv'
import { Connect } from 'types/connect'
import { ResolvedConfig } from '../..'
import { FS_PREFIX } from '../../constants'
import { cleanUrl, isImportRequest } from '../../utils'
import { cleanUrl, fsPathFromId, isImportRequest } from '../../utils'
import { searchForWorkspaceRoot } from '../searchRoot'

const sirvOptions: Options = {
dev: true,
Expand Down Expand Up @@ -74,9 +75,15 @@ export function serveStaticMiddleware(
}
}

export function serveRawFsMiddleware(): Connect.NextHandleFunction {
export function serveRawFsMiddleware(
config: ResolvedConfig
): 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 @@ -86,6 +93,14 @@ export function serveRawFsMiddleware(): Connect.NextHandleFunction {
// the paths are rewritten to `/@fs/` prefixed paths and must be served by
// 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
}

url = url.slice(FS_PREFIX.length)
if (isWin) url = url.replace(/^[A-Z]:/i, '')

Expand All @@ -96,3 +111,29 @@ export function serveRawFsMiddleware(): Connect.NextHandleFunction {
}
}
}

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>
`
}
49 changes: 49 additions & 0 deletions packages/vite/src/node/server/searchRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'fs'
import { dirname } from 'path'
import { join } from 'path'

// https://github.com/vitejs/vite/issues/2820#issuecomment-812495079
const ROOT_FILES = [
// '.git',

// https://pnpm.js.org/workspaces/
'pnpm-workspace.yaml'

// https://rushjs.io/pages/advanced/config_files/
// 'rush.json',

// https://nx.dev/latest/react/getting-started/nx-setup
// 'workspace.json',
// 'nx.json'
]

// npm: https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces
// yarn: https://classic.yarnpkg.com/en/docs/workspaces/#toc-how-to-use-it
function hasWorkspacePackageJSON(root: string): boolean {
const path = join(root, 'package.json')
try {
fs.accessSync(path, fs.constants.R_OK)
} catch {
return false
}
const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {}
return !!content.workspaces
}

function hasRootFile(root: string): boolean {
return ROOT_FILES.some((file) => fs.existsSync(join(root, file)))
}

export function searchForWorkspaceRoot(
current: string,
root = current
): string {
if (hasRootFile(current)) return current
if (hasWorkspacePackageJSON(current)) return current

const dir = dirname(current)
// reach the fs root
if (!dir || dir === current) return root

return searchForWorkspaceRoot(dir, root)
}

0 comments on commit 792a6e1

Please sign in to comment.