Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9da4abc

Browse filesBrowse files
committed
fix!: check host header to prevent DNS rebinding attacks and introduce server.allowedHosts
1 parent b71a5c8 commit 9da4abc
Copy full SHA for 9da4abc

File tree

Expand file treeCollapse file tree

10 files changed

+400
-2
lines changed
Filter options
Expand file treeCollapse file tree

10 files changed

+400
-2
lines changed

‎docs/config/preview-options.md

Copy file name to clipboardExpand all lines: docs/config/preview-options.md
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ See [`server.host`](./server-options#server-host) for more details.
1717

1818
:::
1919

20+
## preview.allowedHosts
21+
22+
- **Type:** `string | true`
23+
- **Default:** [`server.allowedHosts`](./server-options#server-allowedhosts)
24+
25+
The hostnames that Vite is allowed to respond to.
26+
27+
See [`server.allowedHosts`](./server-options#server-allowedhosts) for more details.
28+
2029
## preview.port
2130

2231
- **Type:** `number`

‎docs/config/server-options.md

Copy file name to clipboardExpand all lines: docs/config/server-options.md
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ See [the WSL document](https://learn.microsoft.com/en-us/windows/wsl/networking#
4141

4242
:::
4343

44+
## server.allowedHosts
45+
46+
- **Type:** `string[] | true`
47+
- **Default:** `[]`
48+
49+
The hostnames that Vite is allowed to respond to.
50+
`localhost` and domains under `.localhost` and all IP addresses are allowed by default.
51+
When using HTTPS, this check is skipped.
52+
53+
If a string starts with `.`, it will allow that hostname without the `.` and all subdomains under the hostname. For example, `.example.com` will allow `example.com`, `foo.example.com`, and `foo.bar.example.com`.
54+
55+
If set to `true`, the server is allowed to respond to requests for any hosts.
56+
This is not recommended as it will be vulnerable to DNS rebinding attacks.
57+
4458
## server.port
4559

4660
- **Type:** `number`

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

Copy file name to clipboardExpand all lines: packages/vite/src/node/config.ts
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { findNearestPackageData } from './packages'
7373
import { loadEnv, resolveEnvPrefix } from './env'
7474
import type { ResolvedSSROptions, SSROptions } from './ssr'
7575
import { resolveSSROptions } from './ssr'
76+
import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck'
7677

7778
const debug = createDebugger('vite:config')
7879
const promisifiedRealpath = promisify(fs.realpath)
@@ -424,6 +425,8 @@ export type ResolvedConfig = Readonly<
424425
* @deprecated
425426
*/
426427
webSocketToken: string
428+
/** @internal */
429+
additionalAllowedHosts: string[]
427430
} & PluginHookUtils
428431
>
429432

@@ -791,6 +794,8 @@ export async function resolveConfig(
791794

792795
const base = withTrailingSlash(resolvedBase)
793796

797+
const preview = resolvePreviewOptions(config.preview, server)
798+
794799
resolved = {
795800
configFile: configFile ? normalizePath(configFile) : undefined,
796801
configFileDependencies: configFileDependencies.map((name) =>
@@ -822,7 +827,7 @@ export async function resolveConfig(
822827
},
823828
server,
824829
build: resolvedBuildOptions,
825-
preview: resolvePreviewOptions(config.preview, server),
830+
preview,
826831
envDir,
827832
env: {
828833
...userEnv,
@@ -858,6 +863,7 @@ export async function resolveConfig(
858863
webSocketToken: Buffer.from(
859864
crypto.getRandomValues(new Uint8Array(9)),
860865
).toString('base64url'),
866+
additionalAllowedHosts: getAdditionalAllowedHosts(server, preview),
861867
getSortedPlugins: undefined!,
862868
getSortedPluginHooks: undefined!,
863869
}

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

Copy file name to clipboardExpand all lines: packages/vite/src/node/http.ts
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export interface CommonServerOptions {
2424
* Set to 0.0.0.0 to listen on all addresses, including LAN and public addresses.
2525
*/
2626
host?: string | boolean
27+
/**
28+
* The hostnames that Vite is allowed to respond to.
29+
* `localhost` and subdomains under `.localhost` and all IP addresses are allowed by default.
30+
* When using HTTPS, this check is skipped.
31+
*
32+
* If a string starts with `.`, it will allow that hostname without the `.` and all subdomains under the hostname.
33+
* For example, `.example.com` will allow `example.com`, `foo.example.com`, and `foo.bar.example.com`.
34+
*
35+
* If set to `true`, the server is allowed to respond to requests for any hosts.
36+
* This is not recommended as it will be vulnerable to DNS rebinding attacks.
37+
*/
38+
allowedHosts?: string[] | true
2739
/**
2840
* Enable TLS + HTTP/2.
2941
* Note: this downgrades to TLS only when the proxy option is also used.

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

Copy file name to clipboardExpand all lines: packages/vite/src/node/preview.ts
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { bindCLIShortcuts } from './shortcuts'
3737
import type { BindCLIShortcutsOptions } from './shortcuts'
3838
import { resolveConfig } from './config'
3939
import type { InlineConfig, ResolvedConfig } from './config'
40+
import { hostCheckMiddleware } from './server/middlewares/hostCheck'
4041

4142
export interface PreviewOptions extends CommonServerOptions {}
4243

@@ -53,6 +54,7 @@ export function resolvePreviewOptions(
5354
port: preview?.port,
5455
strictPort: preview?.strictPort ?? server.strictPort,
5556
host: preview?.host ?? server.host,
57+
allowedHosts: preview?.allowedHosts ?? server.allowedHosts,
5658
https: preview?.https ?? server.https,
5759
open: preview?.open ?? server.open,
5860
proxy: preview?.proxy ?? server.proxy,
@@ -188,6 +190,13 @@ export async function preview(
188190
app.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
189191
}
190192

193+
// host check (to prevent DNS rebinding attacks)
194+
const { allowedHosts } = config.preview
195+
// no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks
196+
if (allowedHosts !== true && !config.preview.https) {
197+
app.use(hostCheckMiddleware(config))
198+
}
199+
191200
// proxy
192201
const { proxy } = config.preview
193202
if (proxy) {

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

Copy file name to clipboardExpand all lines: packages/vite/src/node/server/index.ts
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import type { TransformOptions, TransformResult } from './transformRequest'
9797
import { transformRequest } from './transformRequest'
9898
import { searchForWorkspaceRoot } from './searchRoot'
9999
import { warmupFiles } from './warmup'
100+
import { hostCheckMiddleware } from './middlewares/hostCheck'
100101

101102
export interface ServerOptions extends CommonServerOptions {
102103
/**
@@ -853,6 +854,13 @@ export async function _createServer(
853854
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
854855
}
855856

857+
// host check (to prevent DNS rebinding attacks)
858+
const { allowedHosts } = serverConfig
859+
// no need to check for HTTPS as HTTPS is not vulnerable to DNS rebinding attacks
860+
if (allowedHosts !== true && !serverConfig.https) {
861+
middlewares.use(hostCheckMiddleware(config))
862+
}
863+
856864
middlewares.use(cachedTransformMiddleware(server))
857865

858866
// proxy
+112Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, test } from 'vitest'
2+
import {
3+
getAdditionalAllowedHosts,
4+
isHostAllowedWithoutCache,
5+
} from '../hostCheck'
6+
7+
test('getAdditionalAllowedHosts', async () => {
8+
const actual = getAdditionalAllowedHosts(
9+
{
10+
host: 'vite.host.example.com',
11+
hmr: {
12+
host: 'vite.hmr-host.example.com',
13+
},
14+
origin: 'http://vite.origin.example.com:5173',
15+
},
16+
{
17+
host: 'vite.preview-host.example.com',
18+
},
19+
).sort()
20+
expect(actual).toStrictEqual(
21+
[
22+
'vite.host.example.com',
23+
'vite.hmr-host.example.com',
24+
'vite.origin.example.com',
25+
'vite.preview-host.example.com',
26+
].sort(),
27+
)
28+
})
29+
30+
describe('isHostAllowedWithoutCache', () => {
31+
const allowCases = {
32+
'IP address': [
33+
'192.168.0.0',
34+
'[::1]',
35+
'127.0.0.1:5173',
36+
'[2001:db8:0:0:1:0:0:1]:5173',
37+
],
38+
localhost: [
39+
'localhost',
40+
'localhost:5173',
41+
'foo.localhost',
42+
'foo.bar.localhost',
43+
],
44+
specialProtocols: [
45+
// for electron browser window (https://github.com/webpack/webpack-dev-server/issues/3821)
46+
'file:///path/to/file.html',
47+
// for browser extensions (https://github.com/webpack/webpack-dev-server/issues/3807)
48+
'chrome-extension://foo',
49+
],
50+
}
51+
52+
const disallowCases = {
53+
'IP address': ['255.255.255.256', '[:', '[::z]'],
54+
localhost: ['localhos', 'localhost.foo'],
55+
specialProtocols: ['mailto:foo@bar.com'],
56+
others: [''],
57+
}
58+
59+
for (const [name, inputList] of Object.entries(allowCases)) {
60+
test.each(inputList)(`allows ${name} (%s)`, (input) => {
61+
const actual = isHostAllowedWithoutCache([], [], input)
62+
expect(actual).toBe(true)
63+
})
64+
}
65+
66+
for (const [name, inputList] of Object.entries(disallowCases)) {
67+
test.each(inputList)(`disallows ${name} (%s)`, (input) => {
68+
const actual = isHostAllowedWithoutCache([], [], input)
69+
expect(actual).toBe(false)
70+
})
71+
}
72+
73+
test('allows additionalAlloweHosts option', () => {
74+
const additionalAllowedHosts = ['vite.example.com']
75+
const actual = isHostAllowedWithoutCache(
76+
[],
77+
additionalAllowedHosts,
78+
'vite.example.com',
79+
)
80+
expect(actual).toBe(true)
81+
})
82+
83+
test('allows single allowedHosts', () => {
84+
const cases = {
85+
allowed: ['example.com'],
86+
disallowed: ['vite.dev'],
87+
}
88+
for (const c of cases.allowed) {
89+
const actual = isHostAllowedWithoutCache(['example.com'], [], c)
90+
expect(actual, c).toBe(true)
91+
}
92+
for (const c of cases.disallowed) {
93+
const actual = isHostAllowedWithoutCache(['example.com'], [], c)
94+
expect(actual, c).toBe(false)
95+
}
96+
})
97+
98+
test('allows all subdomain allowedHosts', () => {
99+
const cases = {
100+
allowed: ['example.com', 'foo.example.com', 'foo.bar.example.com'],
101+
disallowed: ['vite.dev'],
102+
}
103+
for (const c of cases.allowed) {
104+
const actual = isHostAllowedWithoutCache(['.example.com'], [], c)
105+
expect(actual, c).toBe(true)
106+
}
107+
for (const c of cases.disallowed) {
108+
const actual = isHostAllowedWithoutCache(['.example.com'], [], c)
109+
expect(actual, c).toBe(false)
110+
}
111+
})
112+
})

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.