Skip to content

Commit e96808e

Browse files
committed
fix: return 400 for requests with malformed percent-encoded URIs
Catch URIError thrown by decodeURI() when the request URL contains invalid UTF-8 sequences (e.g. %C0%80) and respond with 400 instead of crashing the server. Expose a configurable onBadUrl callback for custom handling. Closes #118
1 parent c16f074 commit e96808e

File tree

5 files changed

+78
-5
lines changed

5 files changed

+78
-5
lines changed

src/define_config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export function defineConfig(config: UserDefinedServerConfig): ServerConfig {
6464
secure: true,
6565
sameSite: 'lax' as const,
6666
},
67+
onBadUrl(_req, res) {
68+
res.writeHead(400, { 'Content-Type': 'text/plain' })
69+
res.end('Bad Request')
70+
},
6771
qs: {
6872
parse: {
6973
depth: 5,

src/server/main.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,22 @@ export class Server {
427427
* @returns Promise that resolves when request processing is complete
428428
*/
429429
handle(req: IncomingMessage, res: ServerResponse) {
430+
/**
431+
* Reject requests with malformed URIs (e.g. invalid UTF-8
432+
* percent-encoded sequences like %C0%80) before they
433+
* enter the middleware pipeline.
434+
*/
435+
let request: HttpRequest
436+
try {
437+
request = this.createRequest(req, res)
438+
} catch (error) {
439+
if (error instanceof URIError) {
440+
this.#config.onBadUrl(req, res)
441+
return
442+
}
443+
throw error
444+
}
445+
430446
/**
431447
* Setup for the "http:request_finished" event
432448
*/
@@ -437,11 +453,7 @@ export class Server {
437453
* Creating essential instances
438454
*/
439455
const resolver = this.#app.container.createResolver()
440-
const ctx = this.createHttpContext(
441-
this.createRequest(req, res),
442-
this.createResponse(req, res),
443-
resolver
444-
)
456+
const ctx = this.createHttpContext(request, this.createResponse(req, res), resolver)
445457

446458
/**
447459
* Emit event when listening for the request_finished event

src/types/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* file that was distributed with this source code.
88
*/
99

10+
import type { IncomingMessage, ServerResponse } from 'node:http'
1011
import type { Constructor } from '@poppinss/utils/types'
1112
import type { ErrorHandler, FinalHandler } from '@poppinss/middleware/types'
1213

@@ -140,4 +141,14 @@ export type ServerConfig = RequestConfig &
140141
* @default 0 (as per Node.js defaults)
141142
*/
142143
timeout?: number
144+
145+
/**
146+
* A callback invoked when the request URI contains malformed
147+
* percent-encoded sequences (e.g. `%C0%80`). The callback
148+
* receives the raw Node.js request and response objects and
149+
* is responsible for sending a response.
150+
*
151+
* Defaults to a plain-text `400 Bad Request` response.
152+
*/
153+
onBadUrl: (req: IncomingMessage, res: ServerResponse) => void
143154
}

tests/safe_decode_uri.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ test.group('Safe decode URI', () => {
4444
})
4545
})
4646

47+
test('throw URIError on malformed UTF-8 sequences', ({ assert }) => {
48+
assert.throws(() => safeDecodeURI('/%C0%80', false), 'URI malformed')
49+
})
50+
4751
test('decode URI and parse query string params', ({ assert }) => {
4852
const { pathname, query } = safeDecodeURI(
4953
'/a/b/fran%C3%A7ais?a=b&c[0]=1&foo=fran%C3%A7ais',

tests/server.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,48 @@ test.group('Server | Response handling', () => {
408408
slug: 'he/llo',
409409
})
410410
})
411+
412+
test('respond with 400 when request URI contains malformed UTF-8 sequences', async ({
413+
assert,
414+
}) => {
415+
const app = new AppFactory().create(BASE_URL, () => {})
416+
const server = new ServerFactory().merge({ app }).create()
417+
const httpServer = createServer(server.handle.bind(server))
418+
419+
await app.init()
420+
421+
server.use([])
422+
server.getRouter().get('/*', async () => 'handled')
423+
await server.boot()
424+
425+
const { text } = await supertest(httpServer).get('/%C0%80').expect(400)
426+
assert.equal(text, 'Bad Request')
427+
})
428+
429+
test('invoke onBadUrl callback when defined and request URI is malformed', async ({ assert }) => {
430+
const app = new AppFactory().create(BASE_URL, () => {})
431+
const server = new ServerFactory()
432+
.merge({
433+
app,
434+
config: {
435+
onBadUrl(_req, res) {
436+
res.writeHead(400, { 'Content-Type': 'application/json' })
437+
res.end(JSON.stringify({ error: 'Invalid URL' }))
438+
},
439+
},
440+
})
441+
.create()
442+
const httpServer = createServer(server.handle.bind(server))
443+
444+
await app.init()
445+
446+
server.use([])
447+
server.getRouter().get('/*', async () => 'handled')
448+
await server.boot()
449+
450+
const { body } = await supertest(httpServer).get('/%C0%80').expect(400)
451+
assert.deepEqual(body, { error: 'Invalid URL' })
452+
})
411453
})
412454

413455
test.group('Server | middleware', () => {

0 commit comments

Comments
 (0)