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
28 changes: 28 additions & 0 deletions packages/vite/src/node/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getServerUrlByHost,
injectQuery,
isFileReadable,
isParentDirectory,
mergeWithDefaults,
normalizePath,
numberToPos,
Expand Down Expand Up @@ -44,6 +45,33 @@ describe('bareImportRE', () => {
})
})

describe('isParentDirectory', () => {
const cases = {
'/parent': {
'/parent': false,
'/parenta': false,
'/parent/': true,
'/parent/child': true,
'/parent/child/child2': true,
},
'/parent/': {
'/parent': false,
'/parenta': false,
'/parent/': true,
'/parent/child': true,
'/parent/child/child2': true,
},
}

for (const [parent, children] of Object.entries(cases)) {
for (const [child, expected] of Object.entries(children)) {
test(`isParentDirectory("${parent}", "${child}")`, () => {
expect(isParentDirectory(parent, child)).toBe(expected)
})
}
}
})

describe('injectQuery', () => {
if (isWindows) {
// this test will work incorrectly on unix systems
Expand Down
4 changes: 3 additions & 1 deletion packages/vite/src/node/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { notFoundMiddleware } from './server/middlewares/notFound'
import { proxyMiddleware } from './server/middlewares/proxy'
import {
getServerUrlByHost,
normalizePath,
resolveHostname,
resolveServerUrls,
setupSIGTERMListener,
Expand Down Expand Up @@ -263,7 +264,8 @@ export async function preview(

if (config.appType === 'spa' || config.appType === 'mpa') {
// transform index.html
app.use(indexHtmlMiddleware(distDir, server))
const normalizedDistDir = normalizePath(distDir)
app.use(indexHtmlMiddleware(normalizedDistDir, server))

// handle 404s
app.use(notFoundMiddleware())
Expand Down
29 changes: 29 additions & 0 deletions packages/vite/src/node/server/middlewares/__tests__/static.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from 'vitest'
import { isFileInTargetPath } from '../static'

describe('isFileInTargetPath', () => {
const cases = {
'/parent': {
'/parent': true,
'/parenta': false,
'/parent/': true,
'/parent/child': true,
'/parent/child/child2': true,
},
'/parent/': {
'/parent': false,
'/parenta': false,
'/parent/': true,
'/parent/child': true,
'/parent/child/child2': true,
},
}

for (const [parent, children] of Object.entries(cases)) {
for (const [child, expected] of Object.entries(children)) {
test(`isFileInTargetPath("${parent}", "${child}")`, () => {
expect(isFileInTargetPath(parent, child)).toBe(expected)
})
}
}
})
23 changes: 22 additions & 1 deletion packages/vite/src/node/server/middlewares/indexHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
isCSSRequest,
isDevServer,
isJSRequest,
isParentDirectory,
joinUrlSegments,
normalizePath,
processSrcSetSync,
Expand All @@ -48,6 +49,7 @@ import {
BasicMinimalPluginContext,
basePluginContextMeta,
} from '../pluginContainer'
import { checkLoadingAccess, respondWithAccessDenied } from './static'

interface AssetNode {
start: number
Expand Down Expand Up @@ -454,7 +456,26 @@ export function indexHtmlMiddleware(
if (isDev && url.startsWith(FS_PREFIX)) {
filePath = decodeURIComponent(fsPathFromId(url))
} else {
filePath = path.join(root, decodeURIComponent(url))
filePath = normalizePath(
path.resolve(path.join(root, decodeURIComponent(url))),
)
}

if (isDev) {
const servingAccessResult = checkLoadingAccess(server.config, filePath)
if (servingAccessResult === 'denied') {
return respondWithAccessDenied(filePath, server, res)
}
if (servingAccessResult === 'fallback') {
return next()
}
servingAccessResult satisfies 'allowed'
} else {
// `server.fs` options does not apply to the preview server.
// But we should disallow serving files outside the output directory.
if (!isParentDirectory(root, filePath)) {
return next()
}
}

if (fs.existsSync(filePath)) {
Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,10 @@ export function isFileServingAllowed(
* @param targetPath - normalized absolute path
* @param filePath - normalized absolute path
*/
function isFileInTargetPath(targetPath: string, filePath: string) {
export function isFileInTargetPath(
targetPath: string,
filePath: string,
): boolean {
return (
isSameFilePath(targetPath, filePath) ||
isParentDirectory(targetPath, filePath)
Expand Down
23 changes: 23 additions & 0 deletions playground/fs-serve/__tests__/fs-serve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ describe.runIf(isServe)('main', () => {
.toBe('403')
})

test('unsafe HTML fetch', async () => {
await expect
.poll(() => page.textContent('.unsafe-fetch-html'))
.toMatch('403 Restricted')
await expect
.poll(() => page.textContent('.unsafe-fetch-html-status'))
.toBe('403')
})

test('unsafe fetch with special characters (#8498)', async () => {
await expect.poll(() => page.textContent('.unsafe-fetch-8498')).toBe('')
await expect
Expand Down Expand Up @@ -536,4 +545,18 @@ describe.runIf(isServe)('invalid request', () => {
)
expect(response).toContain('HTTP/1.1 403 Forbidden')
})

test('should deny request to HTML file outside root by default with relative path', async () => {
const response = await sendRawRequest(viteTestUrl, '/../unsafe.html')
expect(response).toContain('HTTP/1.1 403 Forbidden')
})
})

describe.runIf(!isServe)('preview HTML', () => {
test('unsafe HTML fetch', async () => {
await expect.poll(() => page.textContent('.unsafe-fetch-html')).toBe('')
await expect
.poll(() => page.textContent('.unsafe-fetch-html-status'))
.toBe('404')
})
})
30 changes: 30 additions & 0 deletions playground/fs-serve/root/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ <h2>Safe Fetch Subdirectory</h2>
<h2>Unsafe Fetch</h2>
<pre class="unsafe-fetch-status"></pre>
<pre class="unsafe-fetch"></pre>
<pre class="unsafe-fetch-html-status"></pre>
<pre class="unsafe-fetch-html"></pre>
<pre class="unsafe-fetch-8498-status"></pre>
<pre class="unsafe-fetch-8498"></pre>
<pre class="unsafe-fetch-8498-2-status"></pre>
Expand All @@ -39,6 +41,8 @@ <h2>Safe /@fs/ Fetch</h2>
<h2>Unsafe /@fs/ Fetch</h2>
<pre class="unsafe-fs-fetch-status"></pre>
<pre class="unsafe-fs-fetch"></pre>
<pre class="unsafe-fs-fetch-html-status"></pre>
<pre class="unsafe-fs-fetch-html"></pre>
<pre class="unsafe-fs-fetch-raw-status"></pre>
<pre class="unsafe-fs-fetch-raw"></pre>
<pre class="unsafe-fs-fetch-raw-query1-status"></pre>
Expand Down Expand Up @@ -149,6 +153,19 @@ <h2>Denied</h2>
console.error(e)
})

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

// outside of allowed dir with special characters #8498
fetch(joinUrlSegments(base, '/src/%2e%2e%2funsafe%2etxt'))
.then((r) => {
Expand Down Expand Up @@ -246,6 +263,19 @@ <h2>Denied</h2>
console.error(e)
})

// not imported before, outside of root, treated as unsafe
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/unsafe.html'))
.then((r) => {
text('.unsafe-fs-fetch-html-status', r.status)
return r.text()
})
.then((data) => {
text('.unsafe-fs-fetch-html', data)
})
.catch((e) => {
console.error(e)
})

// not imported before, outside of root, treated as unsafe
fetch(
joinUrlSegments(
Expand Down
1 change: 1 addition & 0 deletions playground/fs-serve/root/unsafe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>unsafe</p>
1 change: 1 addition & 0 deletions playground/fs-serve/unsafe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>unsafe outside root</p>
Loading