diff --git a/packages/client/src/rpc-json-serializer.test.ts b/packages/client/src/rpc-json-serializer.test.ts index 526353d4b..c32ad8b21 100644 --- a/packages/client/src/rpc-json-serializer.test.ts +++ b/packages/client/src/rpc-json-serializer.test.ts @@ -453,5 +453,33 @@ describe('rpcJsonSerializer: security', () => { expect(Object.hasOwn(restored, '__proto__')).toBe(true) expect(restored.__proto__).toBeInstanceOf(Date) }) + + it('walks an own constructor/prototype chain without reaching the real Object.prototype', () => { + const blob = new Blob(['x']) + + const restored = serializer.deserialize({ + json: JSON.parse('{"constructor": {"prototype": {"when": "2023-01-01T00:00:00.000Z", "polluted": null}}}'), + meta: [['date', 'constructor', 'prototype', 'when']], + maps: [['constructor', 'prototype', 'polluted']], + blobs: [blob], + }) as any + + expect(restored.constructor.prototype.when).toBeInstanceOf(Date) + expect(restored.constructor.prototype.polluted).toBe(blob) + }) + + it('restores a blob stored directly under an own __proto__ key', () => { + const blob = new Blob(['x']) + + const restored = serializer.deserialize({ + json: JSON.parse('{"__proto__": null}'), + maps: [['__proto__']], + blobs: [blob], + }) as any + + expect(Object.hasOwn(restored, '__proto__')).toBe(true) + expect(restored.__proto__).toBe(blob) + expect(Object.getPrototypeOf(restored)).toBe(Object.prototype) + }) /* eslint-enable no-proto, no-restricted-properties */ }) diff --git a/packages/json-schema/src/coercer.ts b/packages/json-schema/src/coercer.ts index 8cd22eaa3..fd424e72f 100644 --- a/packages/json-schema/src/coercer.ts +++ b/packages/json-schema/src/coercer.ts @@ -1,5 +1,5 @@ import type { JsonSchema } from './types' -import { get, isPlainObject, toArray, tryOrUndefined } from '@orpc/shared' +import { get, getOwn, isPlainObject, toArray, tryOrUndefined } from '@orpc/shared' import { decodeJsonPointerSegment } from './ref-utils' import { JsonSchemaXNativeType } from './types' @@ -214,13 +214,12 @@ export class JsonSchemaCoercer { return pattern ? [[pattern, value] as const] : [] }) - const propertySchemas = schema.properties + const propertySchemas: Record = schema.properties ?? {} for (const key in coerced) { const value = coerced[key] - // `properties[key]` alone would resolve keys like `__proto__` to `Object.prototype` - const subSchema = (propertySchemas !== undefined && Object.hasOwn(propertySchemas, key) ? propertySchemas[key] : undefined) + const subSchema = getOwn(propertySchemas, key) ?? patternProperties.find(([pattern]) => pattern.test(key))?.[1] ?? schema.additionalProperties diff --git a/packages/openapi/src/openapi-json-serializer.test.ts b/packages/openapi/src/openapi-json-serializer.test.ts index 567d9177a..fe665ba86 100644 --- a/packages/openapi/src/openapi-json-serializer.test.ts +++ b/packages/openapi/src/openapi-json-serializer.test.ts @@ -296,6 +296,30 @@ describe('openAPIJsonSerializer', () => { expect(Object.hasOwn(result, '__proto__')).toBe(true) expect(result.__proto__.file).toBe(blob) }) + + it('restores a blob stored directly under an own __proto__ key', () => { + const blob = new Blob(['x']) + const result = serializer.deserialize({ + json: JSON.parse('{"__proto__": null}'), + maps: [['__proto__']], + blobs: [blob], + }) as any + + expect(Object.hasOwn(result, '__proto__')).toBe(true) + expect(result.__proto__).toBe(blob) + expect(Object.getPrototypeOf(result)).toBe(Object.prototype) + }) + + it('walks an own constructor/prototype chain without reaching the real Object.prototype', () => { + const blob = new Blob(['x']) + const result = serializer.deserialize({ + json: JSON.parse('{"constructor": {"prototype": {"polluted": null}}}'), + maps: [['constructor', 'prototype', 'polluted']], + blobs: [blob], + }) as any + + expect(result.constructor.prototype.polluted).toBe(blob) + }) /* eslint-enable no-proto, no-restricted-properties */ }) }) diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts index a1617ea79..909f35e91 100644 --- a/packages/shared/src/object.ts +++ b/packages/shared/src/object.ts @@ -81,11 +81,11 @@ export function get(object: unknown, path: readonly PropertyKey[]): unknown { let current: unknown = object for (const key of path) { - if (!isTypescriptObject(current) || !Object.hasOwn(current, key)) { + if (!isTypescriptObject(current)) { return undefined } - current = current[key] + current = getOwn(current, key) } return current @@ -103,7 +103,7 @@ export function set( for (let i = 0; i < path.length - 1; i++) { const key = path[i]! - const next = Object.hasOwn(current, key) ? (current as Record)[key] : undefined + const next = getOwn(current, key) if (!isTypescriptObject(next)) { const child = {}