Skip to content

Commit b09572a

Browse files
committedJan 20, 2025
fix!: default server.cors: false to disallow fetching from untrusted origins
1 parent c0f72a6 commit b09572a

File tree

8 files changed

+105
-9
lines changed

8 files changed

+105
-9
lines changed
 

‎docs/config/server-options.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,15 @@ export default defineConfig({
147147
## server.cors
148148

149149
- **Type:** `boolean | CorsOptions`
150+
- **Default:** `false`
151+
152+
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.
153+
154+
:::warning
150155

151-
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.
156+
We recommend setting a specific value rather than `true` to avoid exposing the source code to untrusted origins.
157+
158+
:::
152159

153160
## server.headers
154161

‎docs/guide/backend-integration.md

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ If you need a custom integration, you can follow the steps in this guide to conf
1212
import { defineConfig } from 'vite'
1313
// ---cut---
1414
export default defineConfig({
15+
server: {
16+
cors: {
17+
// the origin you will be accessing via browser
18+
origin: 'http://my-backend.example.com',
19+
},
20+
},
1521
build: {
1622
// generate .vite/manifest.json in outDir
1723
manifest: true,

‎packages/vite/src/node/__tests__/config.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ describe('preview config', () => {
249249
'Cache-Control': 'no-store',
250250
},
251251
proxy: { '/foo': 'http://localhost:4567' },
252-
cors: false,
252+
cors: true,
253253
})
254254

255255
test('preview inherits server config with default port', async () => {
@@ -285,7 +285,7 @@ describe('preview config', () => {
285285
open: false,
286286
host: false,
287287
proxy: { '/bar': 'http://localhost:3010' },
288-
cors: true,
288+
cors: false,
289289
})
290290

291291
test('preview overrides server config', async () => {

‎packages/vite/src/node/http.ts

+12
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ export interface CommonServerOptions {
5959
/**
6060
* Configure CORS for the dev server.
6161
* Uses https://github.com/expressjs/cors.
62+
*
63+
* When enabling this option, **we recommend setting a specific value
64+
* rather than `true`** to avoid exposing the source code to untrusted origins.
65+
*
6266
* Set to `true` to allow all methods from any origin, or configure separately
6367
* using an object.
68+
*
69+
* @default false
6470
*/
6571
cors?: CorsOptions | boolean
6672
/**
@@ -73,6 +79,12 @@ export interface CommonServerOptions {
7379
* https://github.com/expressjs/cors#configuration-options
7480
*/
7581
export interface CorsOptions {
82+
/**
83+
* Configures the Access-Control-Allow-Origin CORS header.
84+
*
85+
* **We recommend setting a specific value rather than
86+
* `true`** to avoid exposing the source code to untrusted origins.
87+
*/
7688
origin?:
7789
| CorsOrigin
7890
| ((

‎packages/vite/src/node/server/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ export async function _createServer(
851851
middlewares.use(timeMiddleware(root))
852852
}
853853

854-
// cors (enabled by default)
854+
// cors
855855
const { cors } = serverConfig
856856
if (cors !== false) {
857857
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
@@ -1046,7 +1046,7 @@ export const serverConfigDefaults = Object.freeze({
10461046
https: undefined,
10471047
open: false,
10481048
proxy: undefined,
1049-
cors: true,
1049+
cors: false,
10501050
headers: {},
10511051
// hmr
10521052
// ws

‎playground/fs-serve/__tests__/fs-serve.spec.ts

+73-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
import fetch from 'node-fetch'
2-
import { beforeAll, describe, expect, test } from 'vitest'
2+
import {
3+
afterEach,
4+
beforeAll,
5+
beforeEach,
6+
describe,
7+
expect,
8+
test,
9+
} from 'vitest'
10+
import type { Page } from 'playwright-chromium'
311
import testJSON from '../safe.json'
4-
import { isServe, page, viteTestUrl } from '~utils'
12+
import { browser, isServe, page, viteTestUrl } from '~utils'
13+
14+
const getViteTestIndexHtmlUrl = () => {
15+
const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
16+
// NOTE: viteTestUrl is set lazily
17+
return viteTestUrl + srcPrefix + 'src/'
18+
}
519

620
const stringified = JSON.stringify(testJSON)
721

822
describe.runIf(isServe)('main', () => {
923
beforeAll(async () => {
10-
const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
11-
await page.goto(viteTestUrl + srcPrefix + 'src/')
24+
await page.goto(getViteTestIndexHtmlUrl())
1225
})
1326

1427
test('default import', async () => {
@@ -113,3 +126,59 @@ describe('fetch', () => {
113126
expect(res.headers.get('x-served-by')).toBe('vite')
114127
})
115128
})
129+
130+
describe('cross origin', () => {
131+
const fetchStatusFromPage = async (page: Page, url: string) => {
132+
return await page.evaluate(async (url: string) => {
133+
try {
134+
const res = await globalThis.fetch(url)
135+
return res.status
136+
} catch {
137+
return -1
138+
}
139+
}, url)
140+
}
141+
142+
describe('allowed for same origin', () => {
143+
beforeEach(async () => {
144+
await page.goto(getViteTestIndexHtmlUrl())
145+
})
146+
147+
test('fetch HTML file', async () => {
148+
const status = await fetchStatusFromPage(page, viteTestUrl + '/src/')
149+
expect(status).toBe(200)
150+
})
151+
152+
test.runIf(isServe)('fetch JS file', async () => {
153+
const status = await fetchStatusFromPage(
154+
page,
155+
viteTestUrl + '/src/code.js',
156+
)
157+
expect(status).toBe(200)
158+
})
159+
})
160+
161+
describe('denied for different origin', async () => {
162+
let page2: Page
163+
beforeEach(async () => {
164+
page2 = await browser.newPage()
165+
await page2.goto('http://vite.dev/404')
166+
})
167+
afterEach(async () => {
168+
await page2.close()
169+
})
170+
171+
test('fetch HTML file', async () => {
172+
const status = await fetchStatusFromPage(page2, viteTestUrl + '/src/')
173+
expect(status).not.toBe(200)
174+
})
175+
176+
test.runIf(isServe)('fetch JS file', async () => {
177+
const status = await fetchStatusFromPage(
178+
page2,
179+
viteTestUrl + '/src/code.js',
180+
)
181+
expect(status).not.toBe(200)
182+
})
183+
})
184+
})

‎playground/fs-serve/root/src/code.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// code.js

‎playground/fs-serve/root/src/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ <h2>Denied</h2>
5252
<script type="module">
5353
import '../../entry'
5454
import json, { msg } from '../../safe.json'
55+
import './code.js'
5556

5657
function joinUrlSegments(a, b) {
5758
if (!a || !b) {

3 commit comments

Comments
 (3)

timvandam commented on Feb 18, 2025

@timvandam

Hi @sapphi-red, do you know why this behavior was changed in a patch bump (4.5.5 -> 4.5.6)? Does vite ship breaking changes and should we pin its version? Or was this an accident

sapphi-red commented on Feb 18, 2025

@sapphi-red
MemberAuthor

@timvandam Please read GHSA-vg6x-rcgg-rjx6. We don't ship breaking changes in minor and patch, but we ship breaking changes when it's a vuln fix.

timvandam commented on Feb 18, 2025

@timvandam

@sapphi-red I see, thanks for the link!

Please sign in to comment.