Skip to content

Commit 428b976

Browse files
authored
Merge pull request #1564 from elysiajs/next
patch 1.4.17
2 parents 27da2a7 + 837dd7a commit 428b976

File tree

12 files changed

+360
-236
lines changed

12 files changed

+360
-236
lines changed

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1-
# 1.4.17 - TBD
1+
# 1.4.17 - 2 Dec 2025
2+
Improvement:
3+
- [#1573](https://github.com/elysiajs/elysia/pull/1573) `Server` is always resolved to `any` when `@types/bun` is missing
4+
- [#1568](https://github.com/elysiajs/elysia/pull/1568) optimize cookie value comparison using FNV-1a hash
5+
- [#1549](https://github.com/elysiajs/elysia/pull/1549) Set-Cookie headers not sent when errors are thrown
6+
- [#1579](https://github.com/elysiajs/elysia/pull/1579) HEAD to handle Promise value
7+
8+
Security:
9+
- cookie injection, prototype pollution, and RCE
10+
211
Bug fix:
12+
- [#1550](https://github.com/elysiajs/elysia/pull/1550) excess property checking
13+
- allow cookie.sign to be string
14+
15+
Change:
16+
- [#1584](https://github.com/elysiajs/elysia/pull/1584) change customError property to unknown
317
- [#1556](https://github.com/elysiajs/elysia/issues/1556) prevent sending set-cookie header when cookie value is not modified
18+
- [#1563](https://github.com/elysiajs/elysia/issues/1563) standard schema on websocket
19+
- conditional parseQuery parameter
20+
- conditional pass `c.request` to handler for streaming response
21+
- fix missing `contentType` type on `parser`
422

523
# 1.4.16 - 13 Nov 2025
624
Improvement:

bun.lock

Lines changed: 98 additions & 166 deletions
Large diffs are not rendered by default.

example/a.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1-
import { Elysia, status, t } from '../src'
1+
import { Elysia, t } from '../src'
2+
import * as z from 'zod'
3+
import { post, req } from '../test/utils'
24

3-
const auth = (app: Elysia) =>
4-
app.derive(({ headers, status }) => {
5-
try {
6-
const token = headers['authorization']?.replace('Bearer ', '') || ''
7-
return {
8-
isAuthenticated: true
9-
}
10-
} catch (e) {
11-
const error = e as Error
12-
console.error('Authentication error:', error.message)
13-
return status(401, 'Unauthorized')
14-
}
5+
const app = new Elysia({
6+
cookie: { secrets: 'secrets', sign: 'session' }
7+
})
8+
.onError(({ code, error }) => {
9+
console.log({ code })
10+
11+
if (code === 'INVALID_COOKIE_SIGNATURE')
12+
return 'Where is the signature?'
1513
})
14+
.get('/', ({ cookie: { session } }) => 'awd')
15+
16+
console.log(app.routes[0].compile().toString())
1617

17-
const app = new Elysia()
18-
.use(auth)
19-
.get('/', ({ isAuthenticated }) => isAuthenticated)
20-
.listen(5000)
18+
const root = await app.handle(
19+
new Request('http://localhost/', {
20+
headers: {
21+
Cookie: 'session=1234'
22+
}
23+
})
24+
)
2125

22-
app['~Routes']
26+
console.log(await root.text())

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "elysia",
33
"description": "Ergonomic Framework for Human",
4-
"version": "1.4.16",
4+
"version": "1.4.17",
55
"author": {
66
"name": "saltyAom",
77
"url": "https://github.com/SaltyAom",
@@ -190,15 +190,15 @@
190190
"release": "bun run build && bun run test && bun publish"
191191
},
192192
"dependencies": {
193-
"cookie": "^1.0.2",
193+
"cookie": "^1.1.1",
194194
"exact-mirror": "0.2.5",
195195
"fast-decode-uri-component": "^1.0.1",
196196
"memoirist": "^0.4.0"
197197
},
198198
"devDependencies": {
199199
"@elysiajs/openapi": "^1.4.1",
200200
"@types/bun": "^1.2.12",
201-
"@types/cookie": "^1.0.0",
201+
"@types/cookie": "1.0.0",
202202
"@types/fast-decode-uri-component": "^1.0.0",
203203
"@typescript-eslint/eslint-plugin": "^8.30.1",
204204
"@typescript-eslint/parser": "^8.30.1",

src/adapter/bun/index.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ export const BunAdapter: ElysiaAdapter = {
340340
createStaticRoute(app.router.response),
341341
mapRoutes(app)
342342
),
343+
// @ts-ignore
343344
app.config.serve?.routes
344345
),
345346
websocket: {
@@ -360,6 +361,7 @@ export const BunAdapter: ElysiaAdapter = {
360361
createStaticRoute(app.router.response),
361362
mapRoutes(app)
362363
),
364+
// @ts-ignore
363365
app.config.serve?.routes
364366
),
365367
websocket: {
@@ -402,6 +404,7 @@ export const BunAdapter: ElysiaAdapter = {
402404
}),
403405
mapRoutes(app)
404406
),
407+
// @ts-ignore
405408
app.config.serve?.routes
406409
)
407410
})
@@ -428,15 +431,21 @@ export const BunAdapter: ElysiaAdapter = {
428431
// eslint-disable-next-line @typescript-eslint/no-unused-vars
429432
const { parse, body, response, ...rest } = options
430433

431-
const validateMessage = getSchemaValidator(body, {
434+
const messageValidator = getSchemaValidator(body, {
432435
// @ts-expect-error private property
433436
modules: app.definitions.typebox,
434437
// @ts-expect-error private property
435438
models: app.definitions.type as Record<string, TSchema>,
436439
normalize: app.config.normalize
437440
})
438441

439-
const validateResponse = getSchemaValidator(response as any, {
442+
const validateMessage = messageValidator ?
443+
messageValidator.provider === 'standard'
444+
? (data: unknown) => messageValidator.schema['~standard'].validate(data).issues
445+
: (data: unknown) => messageValidator.Check(data) === false
446+
: undefined
447+
448+
const responseValidator = getSchemaValidator(response as any, {
440449
// @ts-expect-error private property
441450
modules: app.definitions.typebox,
442451
// @ts-expect-error private property
@@ -456,7 +465,7 @@ export const BunAdapter: ElysiaAdapter = {
456465
const { set, path, qi, headers, query, params } = context
457466

458467
// @ts-ignore
459-
context.validator = validateResponse
468+
context.validator = responseValidator
460469

461470
if (options.upgrade) {
462471
if (typeof options.upgrade === 'function') {
@@ -484,7 +493,7 @@ export const BunAdapter: ElysiaAdapter = {
484493
set.headers['set-cookie']
485494
) as any
486495

487-
const handleResponse = createHandleWSResponse(validateResponse)
496+
const handleResponse = createHandleWSResponse(responseValidator)
488497
const parseMessage = createWSMessageParser(parse as any)
489498

490499
let _id: string | undefined
@@ -535,7 +544,7 @@ export const BunAdapter: ElysiaAdapter = {
535544

536545
return (_id = randomId())
537546
},
538-
validator: validateResponse,
547+
validator: responseValidator,
539548
ping(ws: ServerWebSocket<any>, data?: unknown) {
540549
options.ping?.(ws as any, data)
541550
},
@@ -560,18 +569,17 @@ export const BunAdapter: ElysiaAdapter = {
560569
) => {
561570
const message = await parseMessage(ws, _message)
562571

563-
if (validateMessage?.Check(message) === false) {
572+
if (validateMessage && validateMessage(message)) {
564573
const validationError = new ValidationError(
565574
'message',
566-
validateMessage,
575+
messageValidator!,
567576
message
568577
)
569578

570-
if (!hasCustomErrorHandlers) {
579+
if (!hasCustomErrorHandlers)
571580
return void ws.send(
572581
validationError.message as string
573582
)
574-
}
575583

576584
return handleErrors(ws, validationError)
577585
}

src/compose.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ import { tee } from './adapter/utils'
6767
const allocateIf = (value: string, condition: unknown) =>
6868
condition ? value : ''
6969

70+
const overrideUnsafeQuote = (value: string) =>
71+
// '`' + value + '`'
72+
'`' + value.replace(/`/g, '\\`').replace(/\${/g, '$\\{') + '`'
73+
74+
const overrideUnsafeQuoteArrayValue = (value: string) =>
75+
value.replace(/`/g, '\\`').replace(/\${/g, '$\\{')
76+
7077
const defaultParsers = [
7178
'json',
7279
'text',
@@ -599,13 +606,17 @@ export const composeHandler = ({
599606
if (cookieMeta.sign === true)
600607
_encodeCookie +=
601608
'for(const [key, cookie] of Object.entries(_setCookie)){' +
602-
`c.set.cookie[key].value=await signCookie(cookie.value,'${secret}')` +
609+
`c.set.cookie[key].value=await signCookie(cookie.value,${!secret ? 'undefined' : overrideUnsafeQuote(secret)})` +
603610
'}'
604-
else
611+
else {
612+
if (typeof cookieMeta.sign === 'string')
613+
cookieMeta.sign = [cookieMeta.sign]
614+
605615
for (const name of cookieMeta.sign)
606616
_encodeCookie +=
607-
`if(_setCookie['${name}']?.value)` +
608-
`c.set.cookie['${name}'].value=await signCookie(_setCookie['${name}'].value,'${secret}')\n`
617+
`if(_setCookie[${overrideUnsafeQuote(name)}]?.value)` +
618+
`c.set.cookie[${overrideUnsafeQuote(name)}].value=await signCookie(_setCookie[${overrideUnsafeQuote(name)}].value,${!secret ? 'undefined' : overrideUnsafeQuote(secret)})\n`
619+
}
609620

610621
_encodeCookie += '}\n'
611622
}
@@ -643,12 +654,16 @@ export const composeHandler = ({
643654
const get = (name: keyof CookieOptions, defaultValue?: unknown) => {
644655
// @ts-ignore
645656
const value = cookieMeta?.[name] ?? defaultValue
657+
658+
if (value === undefined) return ''
659+
646660
if (!value)
647661
return typeof defaultValue === 'string'
648662
? `${name}:"${defaultValue}",`
649663
: `${name}:${defaultValue},`
650664

651-
if (typeof value === 'string') return `${name}:'${value}',`
665+
if (typeof value === 'string')
666+
return `${name}:${overrideUnsafeQuote(value)},`
652667
if (value instanceof Date)
653668
return `${name}: new Date(${value.getTime()}),`
654669

@@ -659,25 +674,25 @@ export const composeHandler = ({
659674
? `{secrets:${
660675
cookieMeta.secrets !== undefined
661676
? typeof cookieMeta.secrets === 'string'
662-
? `'${cookieMeta.secrets}'`
677+
? overrideUnsafeQuote(cookieMeta.secrets)
663678
: '[' +
664-
cookieMeta.secrets.reduce(
665-
(a, b) => a + `'${b}',`,
666-
''
667-
) +
679+
cookieMeta.secrets
680+
.map(overrideUnsafeQuoteArrayValue)
681+
.reduce((a, b) => a + `'${b}',`, '') +
668682
']'
669683
: 'undefined'
670684
},` +
671685
`sign:${
672686
cookieMeta.sign === true
673687
? true
674688
: cookieMeta.sign !== undefined
675-
? '[' +
676-
cookieMeta.sign.reduce(
677-
(a, b) => a + `'${b}',`,
678-
''
679-
) +
680-
']'
689+
? typeof cookieMeta.sign === 'string'
690+
? overrideUnsafeQuote(cookieMeta.sign)
691+
: '[' +
692+
cookieMeta.sign
693+
.map(overrideUnsafeQuoteArrayValue)
694+
.reduce((a, b) => a + `'${b}',`, '') +
695+
']'
681696
: 'undefined'
682697
},` +
683698
get('domain') +
@@ -698,8 +713,8 @@ export const composeHandler = ({
698713
}
699714

700715
if (hasQuery) {
701-
let arrayProperties: Record<string, 1> = {}
702-
let objectProperties: Record<string, 1> = {}
716+
let arrayProperties: Record<string, true> = {}
717+
let objectProperties: Record<string, true> = {}
703718
let hasArrayProperty = false
704719
let hasObjectProperty = false
705720

@@ -709,12 +724,12 @@ export const composeHandler = ({
709724
if (Kind in schema && schema.properties) {
710725
for (const [key, value] of Object.entries(schema.properties)) {
711726
if (hasElysiaMeta('ArrayQuery', value as TSchema)) {
712-
arrayProperties[key] = 1
727+
arrayProperties[key] = true
713728
hasArrayProperty = true
714729
}
715730

716731
if (hasElysiaMeta('ObjectString', value as TSchema)) {
717-
objectProperties[key] = 1
732+
objectProperties[key] = true
718733
hasObjectProperty = true
719734
}
720735
}
@@ -725,12 +740,16 @@ export const composeHandler = ({
725740
'if(c.qi===-1){' +
726741
'c.query=Object.create(null)' +
727742
'}else{' +
728-
`c.query=parseQueryFromURL(c.url,c.qi+1,${
743+
`c.query=parseQueryFromURL(c.url,c.qi+1${
729744
//
730-
hasArrayProperty ? JSON.stringify(arrayProperties) : undefined
731-
},${
745+
hasArrayProperty
746+
? ',' + JSON.stringify(arrayProperties)
747+
: hasObjectProperty
748+
? ',undefined'
749+
: ''
750+
}${
732751
//
733-
hasObjectProperty ? JSON.stringify(objectProperties) : undefined
752+
hasObjectProperty ? ',' + JSON.stringify(objectProperties) : ''
734753
})` +
735754
'}'
736755
}
@@ -837,7 +856,7 @@ export const composeHandler = ({
837856
}
838857

839858
const mapResponseContext =
840-
maybeStream || adapter.mapResponseContext
859+
maybeStream && adapter.mapResponseContext
841860
? `,${adapter.mapResponseContext}`
842861
: ''
843862

src/parse-query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ export function parseQueryFromURL(
8989

9090
const currentValue = result[finalKey]
9191

92-
if (array?.[finalKey]) {
92+
if (array && array?.[finalKey]) {
9393
if (finalValue.charCodeAt(0) === 91) {
94-
if (object?.[finalKey])
94+
if (object && object?.[finalKey])
9595
finalValue = JSON.parse(finalValue) as any
9696
else finalValue = finalValue.slice(1, -1).split(',') as any
9797

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,12 +1395,12 @@ export type BodyHandler<
13951395
Path extends string | undefined = undefined
13961396
> = (
13971397
context: Context<
1398-
Route & {
1398+
Route,
1399+
Singleton & {
13991400
decorator: {
14001401
contentType: string
14011402
}
14021403
},
1403-
Singleton,
14041404
Path
14051405
>,
14061406
/**

src/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export const mergeDeep = <
6868
if (!isObject(target) || !isObject(source)) return target as A & B
6969

7070
for (const [key, value] of Object.entries(source)) {
71-
if (skipKeys?.includes(key)) continue
71+
if (
72+
skipKeys?.includes(key) ||
73+
['__proto__', 'constructor', 'prototype'].includes(key)
74+
)
75+
continue
7276

7377
if (mergeArray && Array.isArray(value)) {
7478
target[key as keyof typeof target] = Array.isArray(

0 commit comments

Comments
 (0)