Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: restrict static middleware fs access #5361

Merged
merged 7 commits into from Oct 23, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 39 additions & 22 deletions packages/playground/fs-serve/__tests__/fs-serve.spec.ts
Expand Up @@ -3,30 +3,47 @@ import { isBuild } from '../../testUtils'
const json = require('../safe.json')
const stringified = JSON.stringify(json)

if (!isBuild) {
test('default import', async () => {
expect(await page.textContent('.full')).toBe(stringified)
describe('main', () => {
beforeAll(async () => {
// viteTestUrl is globally injected in scripts/jestPerTestSetup.ts
await page.goto(viteTestUrl + '/src/')
})

test('named import', async () => {
expect(await page.textContent('.named')).toBe(json.msg)
})
if (!isBuild) {
test('default import', async () => {
expect(await page.textContent('.full')).toBe(stringified)
})

test('safe fetch', async () => {
expect(await page.textContent('.safe-fetch')).toBe(stringified)
expect(await page.textContent('.safe-fetch-status')).toBe('200')
})
test('named import', async () => {
expect(await page.textContent('.named')).toBe(json.msg)
})

test('unsafe fetch', async () => {
expect(await page.textContent('.unsafe-fetch')).toBe('')
expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
})
test('safe fetch', async () => {
expect(await page.textContent('.safe-fetch')).toMatch('KEY=safe')
expect(await page.textContent('.safe-fetch-status')).toBe('200')
})

test('nested entry', async () => {
expect(await page.textContent('.nested-entry')).toBe('foobar')
})
} else {
test('dummy test to make jest happy', async () => {
// Your test suite must contain at least one test.
})
}
test('unsafe fetch', async () => {
expect(await page.textContent('.unsafe-fetch')).toMatch('403 Restricted')
expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
})

test('safe fs fetch', async () => {
expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
})

test('unsafe fs fetch', async () => {
expect(await page.textContent('.unsafe-fs-fetch')).toBe('')
expect(await page.textContent('.unsafe-fs-fetch-status')).toBe('403')
})

test('nested entry', async () => {
expect(await page.textContent('.nested-entry')).toBe('foobar')
})
} else {
test('dummy test to make jest happy', async () => {
// Your test suite must contain at least one test.
})
}
})
1 change: 1 addition & 0 deletions packages/playground/fs-serve/root/.env
@@ -0,0 +1 @@
KEY=unsafe
1 change: 1 addition & 0 deletions packages/playground/fs-serve/root/src/.env
@@ -0,0 +1 @@
KEY=safe
@@ -1,3 +1,5 @@
<link rel="icon" href="/src/favicon.ico" />

<h2>Normal Import</h2>
<pre class="full"></pre>
<pre class="named"></pre>
Expand All @@ -10,34 +12,65 @@ <h2>Unsafe Fetch</h2>
<pre class="unsafe-fetch-status"></pre>
<pre class="unsafe-fetch"></pre>

<h2>Safe /@fs/ Fetch</h2>
<pre class="safe-fs-fetch-status"></pre>
<pre class="safe-fs-fetch"></pre>

<h2>Unsafe /@fs/ Fetch</h2>
<pre class="unsafe-fs-fetch-status"></pre>
<pre class="unsafe-fs-fetch"></pre>

<h2>Nested Entry</h2>
<pre class="nested-entry"></pre>

<script type="module">
import '../entry'
import json, { msg } from '../safe.json'
import '../../entry'
import json, { msg } from '../../safe.json'

text('.full', JSON.stringify(json))
text('.named', msg)

// inside allowed dir, safe fetch
fetch('/src/.env')
.then((r) => {
text('.safe-fetch-status', r.status)
return r.text()
})
.then((data) => {
text('.safe-fetch', JSON.stringify(data))
})

// outside of allowed dir, treated as unsafe
fetch('/.env')
.then((r) => {
text('.unsafe-fetch-status', r.status)
return r.text()
})
.then((data) => {
text('.unsafe-fetch', data)
})
.catch((e) => {
console.error(e)
})

// imported before, should be treated as safe
fetch('/@fs/' + ROOT + '/safe.json')
.then((r) => {
text('.safe-fetch-status', r.status)
text('.safe-fs-fetch-status', r.status)
return r.json()
})
.then((data) => {
text('.safe-fetch', JSON.stringify(data))
text('.safe-fs-fetch', JSON.stringify(data))
})

// not imported before, outside of root, treated as unsafe
fetch('/@fs/' + ROOT + '/unsafe.json')
.then((r) => {
text('.unsafe-fetch-status', r.status)
text('.unsafe-fs-fetch-status', r.status)
return r.json()
})
.then((data) => {
text('.unsafe-fetch', JSON.stringify(data))
text('.unsafe-fs-fetch', JSON.stringify(data))
})
.catch((e) => {
console.error(e)
Expand All @@ -46,4 +79,4 @@ <h2>Nested Entry</h2>
function text(sel, text) {
document.querySelector(sel).textContent = text
}
</script>
</script>
9 changes: 8 additions & 1 deletion packages/playground/fs-serve/root/vite.config.js
Expand Up @@ -4,10 +4,17 @@ const path = require('path')
* @type {import('vite').UserConfig}
*/
module.exports = {
build: {
rollupOptions: {
input: {
main: path.resolve(__dirname, 'src/index.html')
}
}
},
server: {
fs: {
strict: true,
allow: [__dirname]
allow: [path.resolve(__dirname, 'src')]
},
hmr: {
overlay: false
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/index.ts
Expand Up @@ -525,7 +525,7 @@ export async function createServer(

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

// spa fallback
if (!middlewareMode || middlewareMode === 'html') {
Expand Down
16 changes: 13 additions & 3 deletions packages/vite/src/node/server/middlewares/static.ts
@@ -1,7 +1,7 @@
import path from 'path'
import sirv, { Options } from 'sirv'
import { Connect } from 'types/connect'
import { normalizePath, ResolvedConfig, ViteDevServer } from '../..'
import { normalizePath, ViteDevServer } from '../..'
import { FS_PREFIX } from '../../constants'
import {
cleanUrl,
Expand Down Expand Up @@ -45,7 +45,7 @@ export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {

export function serveStaticMiddleware(
dir: string,
config: ResolvedConfig
server: ViteDevServer
): Connect.NextHandleFunction {
const serve = sirv(dir, sirvOptions)

Expand All @@ -66,7 +66,7 @@ export function serveStaticMiddleware(

// apply aliases to static requests as well
let redirected: string | undefined
for (const { find, replacement } of config.resolve.alias) {
for (const { find, replacement } of server.config.resolve.alias) {
const matches =
typeof find === 'string' ? url.startsWith(find) : find.test(url)
if (matches) {
Expand All @@ -79,6 +79,16 @@ export function serveStaticMiddleware(
if (redirected.startsWith(dir)) {
redirected = redirected.slice(dir.length)
}
}

const resolvedUrl = redirected || url
let fileUrl = path.resolve(dir, resolvedUrl.replace(/^\//, ''))
if (resolvedUrl.endsWith('/') && !fileUrl.endsWith('/')) {
fileUrl = fileUrl + '/'
}
ensureServingAccess(fileUrl, server)

if (redirected) {
req.url = redirected
}

Expand Down