Elysia affected by arbitrary code injection through cookie config
Description
Elysia is a Typescript framework for request validation, type inference, OpenAPI documentation and client-server communication. Versions 1.4.17 and below are subject to arbitrary code execution from cookie config. When dynamic cookies are enabled (e.g. there an existing cookie schema), the cookie config is injected into the compiled route without first being sanitised. Availability of this exploit is generally low, but when combined with GHSA-hxj9-33pp-j2cc, it allows for a full RCE chain. An attack requires write access to either the Elysia app's source code (in which case the vulnerability is meaningless) or write access to the cookie config (perhaps where it is assumed to be provisioned by the environment). This issue is fixed in version 1.4.18.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
elysianpm | < 1.4.18 | 1.4.18 |
Affected products
2Patches
23af978663e43:wrench: fix: sanitize cookie key
2 files changed · +4 −4
src/compose.ts+3 −3 modified@@ -606,16 +606,16 @@ export const composeHandler = ({ if (cookieMeta.sign === true) _encodeCookie += 'for(const [key, cookie] of Object.entries(_setCookie)){' + - `c.set.cookie[key].value=await signCookie(cookie.value,\`${secret}\`)` + + `c.set.cookie[key].value=await signCookie(cookie.value,${!secret ? 'undefined' : overrideUnsafeQuote(secret)})` + '}' else { if (typeof cookieMeta.sign === 'string') cookieMeta.sign = [cookieMeta.sign] for (const name of cookieMeta.sign) _encodeCookie += - `if(_setCookie['${name}']?.value)` + - `c.set.cookie['${name}'].value=await signCookie(_setCookie['${name}'].value,\`${secret}\`)\n` + `if(_setCookie[${overrideUnsafeQuote(name)}]?.value)` + + `c.set.cookie[${overrideUnsafeQuote(name)}].value=await signCookie(_setCookie[${overrideUnsafeQuote(name)}].value,${!secret ? 'undefined' : overrideUnsafeQuote(secret)})\n` } _encodeCookie += '}\n'
test/core/elysia.test.ts+1 −1 modified@@ -420,7 +420,7 @@ describe('Edge Case', () => { }) }) - it('handle arbitary code execution from cookie', async () => { + it('handle arbitrary code execution from cookie', async () => { const app = new Elysia({ cookie: { secrets: `\` + console.log(c.q='pwn') + \``,
26935bf76ebc:wrench: fix: cookie injection, prototype pollution, and RCE
5 files changed · +118 −62
CHANGELOG.md+3 −0 modified@@ -5,6 +5,9 @@ Improvement: - [#1549](https://github.com/elysiajs/elysia/pull/1549) Set-Cookie headers not sent when errors are thrown - [#1579](https://github.com/elysiajs/elysia/pull/1579) HEAD to handle Promise value +Security: +- cookie injection, prototype pollution, and RCE + Bug fix: - [#1550](https://github.com/elysiajs/elysia/pull/1550) excess property checking
example/a.ts+19 −46 modified@@ -1,51 +1,24 @@ import { Elysia, t } from '../src' +import * as z from 'zod' +import { post, req } from '../test/utils' -const app = new Elysia().get( - '/', - () => ({ message: 'Hello Elysia' as const }), - { - response: { - 200: t.Object({ - message: t.Literal('Hello Elysia') +const app = new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + data: z.any() + }) + }) + .post('/', ({ body }) => ({ body, win: {}.foo }), { + body: z.object({ + data: z.object({ + messageId: z.string('pollute-me') }) - } - } -) - -type AppResponse = (typeof app)['~Routes']['get']['response'] - -// Should properly infer the 200 response type, not [x: string]: any -const _typeTest: AppResponse extends { - 200: { message: 'Hello Elysia' } -} - ? true - : false = true - -// Test with multiple status codes including 200 -const app2 = new Elysia().post( - '/test', - ({ status }) => { - if (Math.random() > 0.5) { - return status(200, { message: 'Hello Elysia' as const }) - } + }) + }) + .get('/cold-route', () => 'hello world') + .listen(3000) - return status(422, { error: 'Validation error' }) - }, - { - response: { - 200: t.Object({ - message: t.Literal('Hello Elysia') - }), - 422: t.Object({ - error: t.String() - }) - } - } +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) - -type App2Response = (typeof app2)['~Routes']['test']['post']['response'] - -type A = App2Response extends { - 200: { message: 'Hello Elysia' } - 422: { error: string } -} ? true : false
src/compose.ts+18 −12 modified@@ -67,6 +67,10 @@ import { tee } from './adapter/utils' const allocateIf = (value: string, condition: unknown) => condition ? value : '' +const overrideUnsafeQuote = (value: string) => + // '`' + value + '`' + '`' + value.replace(/`/g, '\\`').replace(/\${/g, '$\\{') + '`' + const defaultParsers = [ 'json', 'text', @@ -599,13 +603,13 @@ export const composeHandler = ({ if (cookieMeta.sign === true) _encodeCookie += 'for(const [key, cookie] of Object.entries(_setCookie)){' + - `c.set.cookie[key].value=await signCookie(cookie.value,'${secret}')` + + `c.set.cookie[key].value=await signCookie(cookie.value,\`${secret}\`)` + '}' else for (const name of cookieMeta.sign) _encodeCookie += `if(_setCookie['${name}']?.value)` + - `c.set.cookie['${name}'].value=await signCookie(_setCookie['${name}'].value,'${secret}')\n` + `c.set.cookie['${name}'].value=await signCookie(_setCookie['${name}'].value,\`${secret}\`)\n` _encodeCookie += '}\n' } @@ -643,12 +647,16 @@ export const composeHandler = ({ const get = (name: keyof CookieOptions, defaultValue?: unknown) => { // @ts-ignore const value = cookieMeta?.[name] ?? defaultValue + + if (value === undefined) return '' + if (!value) return typeof defaultValue === 'string' ? `${name}:"${defaultValue}",` : `${name}:${defaultValue},` - if (typeof value === 'string') return `${name}:'${value}',` + if (typeof value === 'string') + return `${name}:${overrideUnsafeQuote(value)},` if (value instanceof Date) return `${name}: new Date(${value.getTime()}),` @@ -659,12 +667,11 @@ export const composeHandler = ({ ? `{secrets:${ cookieMeta.secrets !== undefined ? typeof cookieMeta.secrets === 'string' - ? `'${cookieMeta.secrets}'` + ? overrideUnsafeQuote(cookieMeta.secrets) : '[' + - cookieMeta.secrets.reduce( - (a, b) => a + `'${b}',`, - '' - ) + + cookieMeta.secrets + .map(overrideUnsafeQuote) + .reduce((a, b) => a + `'${b}',`, '') + ']' : 'undefined' },` + @@ -673,10 +680,9 @@ export const composeHandler = ({ ? true : cookieMeta.sign !== undefined ? '[' + - cookieMeta.sign.reduce( - (a, b) => a + `'${b}',`, - '' - ) + + cookieMeta.sign + .map(overrideUnsafeQuote) + .reduce((a, b) => a + `'${b}',`, '') + ']' : 'undefined' },` +
src/utils.ts+5 −1 modified@@ -68,7 +68,11 @@ export const mergeDeep = < if (!isObject(target) || !isObject(source)) return target as A & B for (const [key, value] of Object.entries(source)) { - if (skipKeys?.includes(key)) continue + if ( + skipKeys?.includes(key) || + ['__proto__', 'constructor', 'prototype'].includes(key) + ) + continue if (mergeArray && Array.isArray(value)) { target[key as keyof typeof target] = Array.isArray(
test/core/elysia.test.ts+73 −3 modified@@ -2,6 +2,7 @@ import { Elysia, t } from '../../src' import { describe, expect, it } from 'bun:test' import { req } from '../utils' +import z from 'zod' describe('Edge Case', () => { it('handle state', async () => { @@ -244,11 +245,12 @@ describe('Edge Case', () => { expect(responses).toEqual(['AB', 'BA']) }) - it('handle complex union with json accelerate, exact mirror, and sanitize', async () => { + it('handle complex union with json exact mirror, and sanitize', async () => { const app = new Elysia({ sanitize: (v) => v && 'Elysia' }).get( '/', + // @ts-ignore () => ({ type: 'ok', data: [ @@ -399,7 +401,7 @@ describe('Edge Case', () => { expect(response.status).toBe(200) expect(response.headers.toJSON()).toEqual({ - 'content-length': '11', + 'content-length': '11' }) }) @@ -414,7 +416,75 @@ describe('Edge Case', () => { expect(response.status).toBe(200) expect(response.headers.toJSON()).toEqual({ - 'content-length': '11', + 'content-length': '11' }) }) + + it('handle arbitary code execution from cookie', async () => { + const app = new Elysia({ + cookie: { + secrets: `\` + console.log(c.q='pwn') + \``, + domain: process.env.COOKIE_DOMAIN || 'localhost' + } + }).get( + '/', + (c) => + // @ts-ignore + c.q ?? 'safe', + { + cookie: t.Cookie({ + foo: t.Optional(t.Any()) + }) + } + ) + + const response = await app + .handle(req('/?name=saltyaom')) + .then((x) => x.text()) + + expect(response).toBe('safe') + }) + + it('prototype pollution from input', () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + data: z.any() + }) + }) + .post( + '/', + ({ body }) => ({ + body, + win: + // @ts-ignore + {}.foo + }), + { + body: z.object({ + data: z.object({ + messageId: z.string('pollute-me') + }) + }) + } + ) + + app.handle( + new Request('http://localhost:3000/', { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: `{ + "data": { + "messageId": "pollute-me", + "__proto__": { + "foo": "bar" + } + } + }` + }) + ).then((x) => x.json()) + }) })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-8vch-m3f4-q8jfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66457ghsaADVISORY
- github.com/elysiajs/elysia/commit/26935bf76ebc43b4a43d48b173fc853de43bb51eghsax_refsource_MISCWEB
- github.com/elysiajs/elysia/commit/3af978663e437dccc6c1a2a3aff4b74e1574849eghsax_refsource_MISCWEB
- github.com/elysiajs/elysia/pull/1564ghsax_refsource_MISCWEB
- github.com/elysiajs/elysia/security/advisories/GHSA-8vch-m3f4-q8jfghsax_refsource_CONFIRMWEB
- github.com/elysiajs/elysia/security/advisories/GHSA-hxj9-33pp-j2ccghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.