Skip to content

Commit 9167d2b

Browse files
jameskranzclaude
andcommitted
fix: guard against primitive values in contract router tree traversal
Apply the same primitive type guards to enhanceContractRouter, minifyContractRouter, and populateContractRouterPaths in the contract package. These functions have the same infinite recursion vulnerability when router modules export string values alongside contract procedures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 24438df commit 9167d2b

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

packages/contract/src/router-utils.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,80 @@ it('minifyContractRouter', () => {
7474
expect((minified as any).nested.pong).toEqual(minifiedPong)
7575
})
7676

77+
describe('enhanceContractRouter with primitive values', () => {
78+
const options = { errorMap: {}, prefix: '/test', tags: ['test'] } as const
79+
80+
it('handles string values without infinite recursion', () => {
81+
const routerWithString = { ping, SOME_CONSTANT: 'hello' } as any
82+
const enhanced = enhanceContractRouter(routerWithString, options)
83+
expect(isContractProcedure(enhanced.ping)).toBe(true)
84+
})
85+
86+
it('handles single-character string without infinite recursion', () => {
87+
const routerWithChar = { ping, FLAG: 'x' } as any
88+
const enhanced = enhanceContractRouter(routerWithChar, options)
89+
expect(isContractProcedure(enhanced.ping)).toBe(true)
90+
})
91+
92+
it('handles number values without infinite recursion', () => {
93+
const routerWithNumber = { ping, VERSION: 42 } as any
94+
const enhanced = enhanceContractRouter(routerWithNumber, options)
95+
expect(isContractProcedure(enhanced.ping)).toBe(true)
96+
})
97+
98+
it('handles boolean values without infinite recursion', () => {
99+
const routerWithBool = { ping, ENABLED: true } as any
100+
const enhanced = enhanceContractRouter(routerWithBool, options)
101+
expect(isContractProcedure(enhanced.ping)).toBe(true)
102+
})
103+
104+
it('handles null and undefined values without infinite recursion', () => {
105+
const routerWithNullish = { ping, NIL: null, UNDEF: undefined } as any
106+
const enhanced = enhanceContractRouter(routerWithNullish, options)
107+
expect(isContractProcedure(enhanced.ping)).toBe(true)
108+
})
109+
})
110+
111+
describe('minifyContractRouter with primitive values', () => {
112+
it('handles string values without infinite recursion', () => {
113+
const routerWithString = { ping, LABEL: 'hello' } as any
114+
const minified = minifyContractRouter(routerWithString)
115+
expect(isContractProcedure((minified as any).ping)).toBe(true)
116+
})
117+
118+
it('handles single-character string without infinite recursion', () => {
119+
const routerWithChar = { ping, FLAG: 'x' } as any
120+
const minified = minifyContractRouter(routerWithChar)
121+
expect(isContractProcedure((minified as any).ping)).toBe(true)
122+
})
123+
124+
it('handles number values without infinite recursion', () => {
125+
const routerWithNumber = { ping, VERSION: 42 } as any
126+
const minified = minifyContractRouter(routerWithNumber)
127+
expect(isContractProcedure((minified as any).ping)).toBe(true)
128+
})
129+
})
130+
131+
describe('populateContractRouterPaths with primitive values', () => {
132+
it('handles string values without infinite recursion', () => {
133+
const routerWithString = { ping: oc.input(inputSchema), LABEL: 'hello' } as any
134+
const populated = populateContractRouterPaths(routerWithString)
135+
expect(isContractProcedure(populated.ping)).toBe(true)
136+
})
137+
138+
it('handles single-character string without infinite recursion', () => {
139+
const routerWithChar = { ping: oc.input(inputSchema), FLAG: 'x' } as any
140+
const populated = populateContractRouterPaths(routerWithChar)
141+
expect(isContractProcedure(populated.ping)).toBe(true)
142+
})
143+
144+
it('handles number values without infinite recursion', () => {
145+
const routerWithNumber = { ping: oc.input(inputSchema), VERSION: 42 } as any
146+
const populated = populateContractRouterPaths(routerWithNumber)
147+
expect(isContractProcedure(populated.ping)).toBe(true)
148+
})
149+
})
150+
77151
it('populateContractRouterPaths', () => {
78152
const contract = {
79153
ping: oc.input(inputSchema),

packages/contract/src/router-utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export function enhanceContractRouter<T extends AnyContractRouter, TErrorMap ext
5353
return enhanced as any
5454
}
5555

56+
if (typeof router !== 'object' || router === null) {
57+
return router as any
58+
}
59+
5660
const enhanced: Record<string, any> = {}
5761

5862
for (const key in router) {
@@ -83,6 +87,10 @@ export function minifyContractRouter(router: AnyContractRouter): AnyContractRout
8387
return procedure
8488
}
8589

90+
if (typeof router !== 'object' || router === null) {
91+
return router as any
92+
}
93+
8694
const json: Record<string, AnyContractRouter> = {}
8795

8896
for (const key in router) {
@@ -128,6 +136,10 @@ export function populateContractRouterPaths<T extends AnyContractRouter>(router:
128136
return router as any
129137
}
130138

139+
if (typeof router !== 'object' || router === null) {
140+
return router as any
141+
}
142+
131143
const populated: Record<string, any> = {}
132144

133145
for (const key in router) {

0 commit comments

Comments
 (0)