From dfea38f1ff9f6fc0f0ca57927c527b0b9ffd2210 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:04:31 +0900 Subject: [PATCH] fix!: default `server.cors: false` to disallow fetching from untrusted origins --- docs/config/server-options.md | 9 ++- docs/guide/backend-integration.md | 6 ++ .../vite/src/node/__tests__/config.spec.ts | 4 +- packages/vite/src/node/http.ts | 12 +++ packages/vite/src/node/preview.ts | 2 +- packages/vite/src/node/server/index.ts | 4 +- .../fs-serve/__tests__/fs-serve.spec.ts | 77 ++++++++++++++++++- playground/fs-serve/root/src/code.js | 1 + playground/fs-serve/root/src/index.html | 1 + 9 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 playground/fs-serve/root/src/code.js diff --git a/docs/config/server-options.md b/docs/config/server-options.md index fe4dbea79850ea..d55b52b42bf3ad 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -137,8 +137,15 @@ export default defineConfig({ ## server.cors - **Type:** `boolean | CorsOptions` +- **Default:** `false` + +Configure CORS for the dev server. Pass an [options object](https://github.com/expressjs/cors#configuration-options) to fine tune the behavior or `true` to allow any origin. + +:::warning -Configure CORS for the dev server. This is enabled by default and allows any origin. Pass an [options object](https://github.com/expressjs/cors#configuration-options) to fine tune the behavior or `false` to disable. +We recommend setting a specific value rather than `true` to avoid exposing the source code to untrusted origins. + +::: ## server.headers diff --git a/docs/guide/backend-integration.md b/docs/guide/backend-integration.md index 8509082bbdf2ea..1fc3a03958231d 100644 --- a/docs/guide/backend-integration.md +++ b/docs/guide/backend-integration.md @@ -13,6 +13,12 @@ If you need a custom integration, you can follow the steps in this guide to conf // ---cut--- // vite.config.js export default defineConfig({ + server: { + cors: { + // the origin you will be accessing via browser + origin: 'http://my-backend.example.com', + }, + }, build: { // generate .vite/manifest.json in outDir manifest: true, diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index 9fbbdd61f9ac7d..3eaa76aa004c45 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -249,7 +249,7 @@ describe('preview config', () => { 'Cache-Control': 'no-store', }, proxy: { '/foo': 'http://localhost:4567' }, - cors: false, + cors: true, }) test('preview inherits server config with default port', async () => { @@ -285,7 +285,7 @@ describe('preview config', () => { open: false, host: false, proxy: { '/bar': 'http://localhost:3010' }, - cors: true, + cors: false, }) test('preview overrides server config', async () => { diff --git a/packages/vite/src/node/http.ts b/packages/vite/src/node/http.ts index ec1bf5e645641d..faaf6e17d17078 100644 --- a/packages/vite/src/node/http.ts +++ b/packages/vite/src/node/http.ts @@ -59,8 +59,14 @@ export interface CommonServerOptions { /** * Configure CORS for the dev server. * Uses https://github.com/expressjs/cors. + * + * When enabling this option, **we recommend setting a specific value + * rather than `true`** to avoid exposing the source code to untrusted origins. + * * Set to `true` to allow all methods from any origin, or configure separately * using an object. + * + * @default false */ cors?: CorsOptions | boolean /** @@ -73,6 +79,12 @@ export interface CommonServerOptions { * https://github.com/expressjs/cors#configuration-options */ export interface CorsOptions { + /** + * Configures the Access-Control-Allow-Origin CORS header. + * + * **We recommend setting a specific value rather than + * `true`** to avoid exposing the source code to untrusted origins. + */ origin?: | CorsOrigin | (( diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 38192abea7626d..88e0a80c30f95e 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -184,7 +184,7 @@ export async function preview( // cors const { cors } = config.preview - if (cors !== false) { + if (cors !== undefined && cors !== false) { app.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index fa75408fafbaff..9fc1e056385132 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -847,9 +847,9 @@ export async function _createServer( middlewares.use(timeMiddleware(root)) } - // cors (enabled by default) + // cors const { cors } = serverConfig - if (cors !== false) { + if (cors !== undefined && cors !== false) { middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 16ecc0b78dc295..b5f799b358b94a 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -1,14 +1,27 @@ import fetch from 'node-fetch' -import { beforeAll, describe, expect, test } from 'vitest' +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from 'vitest' +import type { Page } from 'playwright-chromium' import testJSON from '../safe.json' -import { isServe, page, viteTestUrl } from '~utils' +import { browser, isServe, page, viteTestUrl } from '~utils' + +const getViteTestIndexHtmlUrl = () => { + const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/' + // NOTE: viteTestUrl is set lazily + return viteTestUrl + srcPrefix + 'src/' +} const stringified = JSON.stringify(testJSON) describe.runIf(isServe)('main', () => { beforeAll(async () => { - const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/' - await page.goto(viteTestUrl + srcPrefix + 'src/') + await page.goto(getViteTestIndexHtmlUrl()) }) test('default import', async () => { @@ -113,3 +126,59 @@ describe('fetch', () => { expect(res.headers.get('x-served-by')).toBe('vite') }) }) + +describe('cross origin', () => { + const fetchStatusFromPage = async (page: Page, url: string) => { + return await page.evaluate(async (url: string) => { + try { + const res = await globalThis.fetch(url) + return res.status + } catch { + return -1 + } + }, url) + } + + describe('allowed for same origin', () => { + beforeEach(async () => { + await page.goto(getViteTestIndexHtmlUrl()) + }) + + test('fetch HTML file', async () => { + const status = await fetchStatusFromPage(page, viteTestUrl + '/src/') + expect(status).toBe(200) + }) + + test.runIf(isServe)('fetch JS file', async () => { + const status = await fetchStatusFromPage( + page, + viteTestUrl + '/src/code.js', + ) + expect(status).toBe(200) + }) + }) + + describe('denied for different origin', async () => { + let page2: Page + beforeEach(async () => { + page2 = await browser.newPage() + await page2.goto('http://vite.dev/404') + }) + afterEach(async () => { + await page2.close() + }) + + test('fetch HTML file', async () => { + const status = await fetchStatusFromPage(page2, viteTestUrl + '/src/') + expect(status).not.toBe(200) + }) + + test.runIf(isServe)('fetch JS file', async () => { + const status = await fetchStatusFromPage( + page2, + viteTestUrl + '/src/code.js', + ) + expect(status).not.toBe(200) + }) + }) +}) diff --git a/playground/fs-serve/root/src/code.js b/playground/fs-serve/root/src/code.js new file mode 100644 index 00000000000000..33fd8df878207b --- /dev/null +++ b/playground/fs-serve/root/src/code.js @@ -0,0 +1 @@ +// code.js diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html index fb1276d79fea22..9f56c8831c12d9 100644 --- a/playground/fs-serve/root/src/index.html +++ b/playground/fs-serve/root/src/index.html @@ -52,6 +52,7 @@