Skip to content

Commit 823675b

Browse files
authored
fix: backport #19830, reject requests with # in request-target (#19831)
1 parent 0a2518a commit 823675b

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { transformRequest } from './transformRequest'
102102
import { searchForWorkspaceRoot } from './searchRoot'
103103
import { warmupFiles } from './warmup'
104104
import { hostCheckMiddleware } from './middlewares/hostCheck'
105+
import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest'
105106

106107
export interface ServerOptions extends CommonServerOptions {
107108
/**
@@ -852,6 +853,9 @@ export async function _createServer(
852853
middlewares.use(timeMiddleware(root))
853854
}
854855

856+
// disallows request that contains `#` in the URL
857+
middlewares.use(rejectInvalidRequestMiddleware())
858+
855859
// cors
856860
const { cors } = serverConfig
857861
if (cors !== false) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Connect } from 'dep-types/connect'
2+
3+
export function rejectInvalidRequestMiddleware(): Connect.NextHandleFunction {
4+
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
5+
return function viteRejectInvalidRequestMiddleware(req, res, next) {
6+
// HTTP spec does not allow `#` in the request-target
7+
// (HTTP 1.1: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2)
8+
// (HTTP 2: https://datatracker.ietf.org/doc/html/rfc9113#section-8.3.1-2.4.1)
9+
// But Node.js allows those requests.
10+
// Our middlewares don't expect `#` to be included in `req.url`, especially the `server.fs.deny` checks.
11+
if (req.url?.includes('#')) {
12+
// HTTP 1.1 spec recommends sending 400 Bad Request
13+
// (https://datatracker.ietf.org/doc/html/rfc9112#section-3.2-4)
14+
res.writeHead(400)
15+
res.end()
16+
return
17+
}
18+
return next()
19+
}
20+
}

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

+75
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import net from 'node:net'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
14
import fetch from 'node-fetch'
25
import {
36
afterEach,
@@ -12,6 +15,8 @@ import WebSocket from 'ws'
1215
import testJSON from '../safe.json'
1316
import { browser, isServe, page, viteServer, viteTestUrl } from '~utils'
1417

18+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
19+
1520
const getViteTestIndexHtmlUrl = () => {
1621
const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
1722
// NOTE: viteTestUrl is set lazily
@@ -391,3 +396,73 @@ describe('cross origin', () => {
391396
)
392397
})
393398
})
399+
400+
describe.runIf(isServe)('invalid request', () => {
401+
const sendRawRequest = async (baseUrl: string, requestTarget: string) => {
402+
return new Promise<string>((resolve, reject) => {
403+
const parsedUrl = new URL(baseUrl)
404+
405+
const buf: Buffer[] = []
406+
const client = net.createConnection(
407+
{ port: +parsedUrl.port, host: parsedUrl.hostname },
408+
() => {
409+
client.write(
410+
[
411+
`GET ${encodeURI(requestTarget)} HTTP/1.1`,
412+
`Host: ${parsedUrl.host}`,
413+
'Connection: Close',
414+
'\r\n',
415+
].join('\r\n'),
416+
)
417+
},
418+
)
419+
client.on('data', (data) => {
420+
buf.push(data)
421+
})
422+
client.on('end', (hadError) => {
423+
if (!hadError) {
424+
resolve(Buffer.concat(buf).toString())
425+
}
426+
})
427+
client.on('error', (err) => {
428+
reject(err)
429+
})
430+
})
431+
}
432+
433+
const root = path
434+
.resolve(__dirname.replace('playground', 'playground-temp'), '..')
435+
.replace(/\\/g, '/')
436+
437+
test('request with sendRawRequest should work', async () => {
438+
const response = await sendRawRequest(viteTestUrl, '/src/safe.txt')
439+
expect(response).toContain('HTTP/1.1 200 OK')
440+
expect(response).toContain('KEY=safe')
441+
})
442+
443+
test('request with sendRawRequest should work with /@fs/', async () => {
444+
const response = await sendRawRequest(
445+
viteTestUrl,
446+
path.posix.join('/@fs/', root, 'root/src/safe.txt'),
447+
)
448+
expect(response).toContain('HTTP/1.1 200 OK')
449+
expect(response).toContain('KEY=safe')
450+
})
451+
452+
test('should reject request that has # in request-target', async () => {
453+
const response = await sendRawRequest(
454+
viteTestUrl,
455+
'/src/safe.txt#/../../unsafe.txt',
456+
)
457+
expect(response).toContain('HTTP/1.1 400 Bad Request')
458+
})
459+
460+
test('should reject request that has # in request-target with /@fs/', async () => {
461+
const response = await sendRawRequest(
462+
viteTestUrl,
463+
path.posix.join('/@fs/', root, 'root/src/safe.txt') +
464+
'#/../../unsafe.txt',
465+
)
466+
expect(response).toContain('HTTP/1.1 400 Bad Request')
467+
})
468+
})

0 commit comments

Comments
 (0)