Skip to content

Commit 41f3819

Browse files
authored
fix: backport #19830, reject requests with # in request-target (#19832)
1 parent 6104add commit 41f3819

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
@@ -83,6 +83,7 @@ import type { TransformOptions, TransformResult } from './transformRequest'
8383
import { transformRequest } from './transformRequest'
8484
import { searchForWorkspaceRoot } from './searchRoot'
8585
import { hostCheckMiddleware } from './middlewares/hostCheck'
86+
import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest'
8687

8788
export interface ServerOptions extends CommonServerOptions {
8889
/**
@@ -616,6 +617,9 @@ export async function _createServer(
616617
middlewares.use(timeMiddleware(root))
617618
}
618619

620+
// disallows request that contains `#` in the URL
621+
middlewares.use(rejectInvalidRequestMiddleware())
622+
619623
// cors
620624
const { cors } = serverConfig
621625
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
@@ -392,3 +397,73 @@ describe('cross origin', () => {
392397
)
393398
})
394399
})
400+
401+
describe.runIf(isServe)('invalid request', () => {
402+
const sendRawRequest = async (baseUrl: string, requestTarget: string) => {
403+
return new Promise<string>((resolve, reject) => {
404+
const parsedUrl = new URL(baseUrl)
405+
406+
const buf: Buffer[] = []
407+
const client = net.createConnection(
408+
{ port: +parsedUrl.port, host: parsedUrl.hostname },
409+
() => {
410+
client.write(
411+
[
412+
`GET ${encodeURI(requestTarget)} HTTP/1.1`,
413+
`Host: ${parsedUrl.host}`,
414+
'Connection: Close',
415+
'\r\n',
416+
].join('\r\n'),
417+
)
418+
},
419+
)
420+
client.on('data', (data) => {
421+
buf.push(data)
422+
})
423+
client.on('end', (hadError) => {
424+
if (!hadError) {
425+
resolve(Buffer.concat(buf).toString())
426+
}
427+
})
428+
client.on('error', (err) => {
429+
reject(err)
430+
})
431+
})
432+
}
433+
434+
const root = path
435+
.resolve(__dirname.replace('playground', 'playground-temp'), '..')
436+
.replace(/\\/g, '/')
437+
438+
test('request with sendRawRequest should work', async () => {
439+
const response = await sendRawRequest(viteTestUrl, '/src/safe.txt')
440+
expect(response).toContain('HTTP/1.1 200 OK')
441+
expect(response).toContain('KEY=safe')
442+
})
443+
444+
test('request with sendRawRequest should work with /@fs/', async () => {
445+
const response = await sendRawRequest(
446+
viteTestUrl,
447+
path.posix.join('/@fs/', root, 'root/src/safe.txt'),
448+
)
449+
expect(response).toContain('HTTP/1.1 200 OK')
450+
expect(response).toContain('KEY=safe')
451+
})
452+
453+
test('should reject request that has # in request-target', async () => {
454+
const response = await sendRawRequest(
455+
viteTestUrl,
456+
'/src/safe.txt#/../../unsafe.txt',
457+
)
458+
expect(response).toContain('HTTP/1.1 400 Bad Request')
459+
})
460+
461+
test('should reject request that has # in request-target with /@fs/', async () => {
462+
const response = await sendRawRequest(
463+
viteTestUrl,
464+
path.posix.join('/@fs/', root, 'root/src/safe.txt') +
465+
'#/../../unsafe.txt',
466+
)
467+
expect(response).toContain('HTTP/1.1 400 Bad Request')
468+
})
469+
})

0 commit comments

Comments
 (0)