From 07b36d503a4448cf2b8787a4eb1ccdaefb3424b5 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 ade62028b6754d..01325f22694c0f 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -135,8 +135,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 16ca394ce769a4..93e09fb759d206 100644 --- a/docs/guide/backend-integration.md +++ b/docs/guide/backend-integration.md @@ -11,6 +11,12 @@ If you need a custom integration, you can follow the steps in this guide to conf ```js // vite.config.js export default defineConfig({ + server: { + cors: { + // the origin you will be accessing via browser + origin: 'http://my-backend.example.com', + }, + }, build: { // generate 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 b4196af570d207..a2f5dbc16221ce 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -237,7 +237,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 () => { @@ -274,7 +274,7 @@ describe('preview config', () => { host: false, https: 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 afefc14fc5e1d0..1460c80a1b5a63 100644 --- a/packages/vite/src/node/http.ts +++ b/packages/vite/src/node/http.ts @@ -62,8 +62,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 /** @@ -76,6 +82,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 | ((origin: string, cb: (err: Error, origins: CorsOrigin) => void) => void) diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 460366446a2f3c..4321a5e9cbef95 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -144,7 +144,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 1acc6dde532ee8..d76788eadd7a2c 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -611,9 +611,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 @@