High severity7.5NVD Advisory· Published Feb 5, 2025· Updated Apr 15, 2026
CVE-2024-57068
CVE-2024-57068
Description
A prototype pollution in the lib.mutateMergeDeep function of @tanstack/form-core v0.35.0 allows attackers to cause a Denial of Service (DoS) via supplying a crafted payload.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@tanstack/form-corenpm | < 0.42.1 | 0.42.1 |
Patches
1455522c8f327fix(form-core): prevent prototype pollution and update Remix dependency - CVE-2024-57068 (#1151)
7 files changed · +801 −368
docs/reference/functions/mergeform.md+1 −1 modified@@ -9,7 +9,7 @@ title: mergeForm function mergeForm<TFormData, TFormValidator>(baseForm, state): FormApi<NoInfer<TFormData>, NoInfer<TFormValidator>> ``` -Defined in: [packages/form-core/src/mergeForm.ts:36](https://github.com/TanStack/form/blob/main/packages/form-core/src/mergeForm.ts#L36) +Defined in: [packages/form-core/src/mergeForm.ts:75](https://github.com/TanStack/form/blob/main/packages/form-core/src/mergeForm.ts#L75) ## Type Parameters
examples/react/remix/package.json+1 −1 modified@@ -8,7 +8,7 @@ "_test:types": "tsc" }, "dependencies": { - "@remix-run/node": "^2.15.0", + "@remix-run/node": "^2.15.3", "@remix-run/react": "^2.15.0", "@remix-run/serve": "^2.15.0", "@tanstack/react-form": "^0.42.0",
packages/form-core/src/mergeForm.ts+58 −19 modified@@ -2,34 +2,73 @@ import type { FormApi } from './FormApi' import type { Validator } from './types' import type { NoInfer } from './util-types' +function isValidKey(key: string | number | symbol): boolean { + const dangerousProps = ['__proto__', 'constructor', 'prototype'] + return !dangerousProps.includes(String(key)) +} + /** * @private */ -export function mutateMergeDeep(target: object, source: object): object { +export function mutateMergeDeep( + target: object | null | undefined, + source: object | null | undefined, +): object { + // Early return if either is not an object + if (target === null || target === undefined || typeof target !== 'object') + return {} as object + if (source === null || source === undefined || typeof source !== 'object') + return target + const targetKeys = Object.keys(target) const sourceKeys = Object.keys(source) const keySet = new Set([...targetKeys, ...sourceKeys]) + for (const key of keySet) { - const targetKey = key as never as keyof typeof target - const sourceKey = key as never as keyof typeof source - - if (Array.isArray(target[targetKey]) && Array.isArray(source[sourceKey])) { - // always use the source array to prevent array fields from multiplying - target[targetKey] = source[sourceKey] as [] as never - } else if ( - typeof target[targetKey] === 'object' && - typeof source[sourceKey] === 'object' - ) { - mutateMergeDeep(target[targetKey] as {}, source[sourceKey] as {}) - } else { - // Prevent assigning undefined to target, only if undefined is not explicitly set on source - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!(sourceKey in source) && source[sourceKey] === undefined) { - continue - } - target[targetKey] = source[sourceKey] as never + if (!isValidKey(key)) continue + + const targetKey = key as keyof typeof target + const sourceKey = key as keyof typeof source + + if (!Object.hasOwn(source, sourceKey)) continue + + const sourceValue = source[sourceKey] as unknown + const targetValue = target[targetKey] as unknown + + // Handle arrays + if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { + Object.defineProperty(target, key, { + value: [...sourceValue], + enumerable: true, + writable: true, + configurable: true, + }) + continue + } + + // Handle nested objects (type assertion to satisfy ESLint) + const isTargetObj = typeof targetValue === 'object' && targetValue !== null + const isSourceObj = typeof sourceValue === 'object' && sourceValue !== null + const areObjects = + isTargetObj && + isSourceObj && + !Array.isArray(targetValue) && + !Array.isArray(sourceValue) + + if (areObjects) { + mutateMergeDeep(targetValue as object, sourceValue as object) + continue } + + // Handle all other cases + Object.defineProperty(target, key, { + value: sourceValue, + enumerable: true, + writable: true, + configurable: true, + }) } + return target }
packages/form-core/tests/mergeForm.spec.ts+100 −0 added@@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest' +import { mutateMergeDeep } from '../src/mergeForm' + +type TestObject = Record<string, any> + +describe('mutateMergeDeep', () => { + it('should prevent prototype pollution through __proto__', () => { + const target: TestObject = {} + const malicious = { + __proto__: { + polluted: true, + }, + } + + mutateMergeDeep(target, malicious) + expect(({} as TestObject).polluted).toBeUndefined() + expect((Object.prototype as TestObject).polluted).toBeUndefined() + }) + + it('should prevent prototype pollution through constructor', () => { + const target: TestObject = {} + const malicious = { + constructor: { + prototype: { + polluted: true, + }, + }, + } + + mutateMergeDeep(target, malicious) + expect(({} as TestObject).polluted).toBeUndefined() + }) + + it('should handle null values correctly', () => { + const target = { details: null } + const source = { details: { age: 25 } } + + mutateMergeDeep(target, source) + expect(target).toStrictEqual({ details: { age: 25 } }) + }) + + it('should preserve object references when updating nested objects', () => { + const target: { user: { details: TestObject } } = { user: { details: {} } } + const source = { user: { details: { name: 'test' } } } + + const originalDetails = target.user.details + mutateMergeDeep(target, source) + expect(target.user.details).toBe(originalDetails) + expect(target.user.details.name).toBe('test') + }) + + it('Should merge two objects by mutating', () => { + const a = { a: 1 } + const b = { b: 2 } + mutateMergeDeep(a, b) + expect(a).toStrictEqual({ a: 1, b: 2 }) + }) + + it('Should merge two objects including overwriting with undefined', () => { + const a = { a: 1 } + const b = { a: undefined } + mutateMergeDeep(a, b) + expect(a).toStrictEqual({ a: undefined }) + }) + + it('Should merge two object by overriding arrays', () => { + const target = { a: [1] } + const source = { a: [2] } + mutateMergeDeep(target, source) + expect(target).toStrictEqual({ a: [2] }) + }) + + it('Should merge add array element when it does not exist in target', () => { + const target = { a: [] } + const source = { a: [2] } + mutateMergeDeep(target, source) + expect(target).toStrictEqual({ a: [2] }) + }) + + it('Should override the target array if source is undefined', () => { + const target = { a: [2] } + const source = { a: undefined } + mutateMergeDeep(target, source) + expect(target).toStrictEqual({ a: undefined }) + }) + + it('Should merge update array element when it does not exist in source', () => { + const target = { a: [2] } + const source = { a: [] } + mutateMergeDeep(target, source) + expect(target).toStrictEqual({ a: [] }) + }) + + it('Should merge two deeply nested objects', () => { + const a = { a: { a: 1 } } + const b = { a: { b: 2 } } + mutateMergeDeep(a, b) + expect(a).toStrictEqual({ a: { a: 1, b: 2 } }) + }) +})
packages/form-core/tests/mutateMergeDeep.spec.ts+0 −53 removed@@ -1,53 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { mutateMergeDeep } from '../src/index' - -describe('mutateMergeDeep', () => { - test('Should merge two objects by mutating', () => { - const a = { a: 1 } - const b = { b: 2 } - mutateMergeDeep(a, b) - expect(a).toStrictEqual({ a: 1, b: 2 }) - }) - - test('Should merge two objects including overwriting with undefined', () => { - const a = { a: 1 } - const b = { a: undefined } - mutateMergeDeep(a, b) - expect(a).toStrictEqual({ a: undefined }) - }) - - test('Should merge two object by overriding arrays', () => { - const target = { a: [1] } - const source = { a: [2] } - mutateMergeDeep(target, source) - expect(target).toStrictEqual({ a: [2] }) - }) - - test('Should merge add array element when it does not exist in target', () => { - const target = { a: [] } - const source = { a: [2] } - mutateMergeDeep(target, source) - expect(target).toStrictEqual({ a: [2] }) - }) - - test('Should override the target array if source is undefined', () => { - const target = { a: [2] } - const source = { a: undefined } - mutateMergeDeep(target, source) - expect(target).toStrictEqual({ a: undefined }) - }) - - test('Should merge update array element when it does not exist in source', () => { - const target = { a: [2] } - const source = { a: [] } - mutateMergeDeep(target, source) - expect(target).toStrictEqual({ a: [] }) - }) - - test('Should merge two deeply nested objects', () => { - const a = { a: { a: 1 } } - const b = { a: { b: 2 } } - mutateMergeDeep(a, b) - expect(a).toStrictEqual({ a: { a: 1, b: 2 } }) - }) -})
packages/react-form/package.json+1 −1 modified@@ -82,7 +82,7 @@ "src" ], "dependencies": { - "@remix-run/node": "^2.15.0", + "@remix-run/node": "^2.15.3", "@tanstack/form-core": "workspace:*", "@tanstack/react-store": "^0.7.0", "decode-formdata": "^0.8.0"
pnpm-lock.yaml+640 −293 modified
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
5News mentions
0No linked articles in our index yet.