Prototype Pollution vulnerability in @clickbar/dot-diver
Description
Dot diver is a lightweight, powerful, and dependency-free TypeScript utility library that provides types and functions to work with object paths in dot notation. In versions prior to 1.0.2 there is a Prototype Pollution vulnerability in the setByPath function which can leads to remote code execution (RCE). This issue has been addressed in commit 98daf567 which has been included in release 1.0.2. Users are advised to upgrade. There are no known workarounds to this vulnerability.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A prototype pollution vulnerability in dot-diver's setByPath function could allow RCE; fixed in v1.0.2.
Vulnerability
Overview
A prototype pollution vulnerability exists in the setByPath function of the @clickbar/dot-diver TypeScript utility library, versions prior to 1.0.2 [1][2]. The root cause is that the function fails to validate whether the path parts being set refer to the object's own properties, allowing an attacker to traverse and modify the object's prototype chain [3][4].
Exploitation
Method
To exploit this flaw, an attacker must control the path or value argument passed to setByPath. When a path like __proto__.polluted or constructor.prototype.polluted is provided, the function sets the value directly onto Object.prototype instead of the intended object, thus polluting all objects in the runtime [1][2]. No authentication or special privileges are required if the application processes user-supplied path strings.
Impact
Successful exploitation leads to prototype pollution, which can be escalated to remote code execution (RCE) in the context of the application [2]. By polluting properties that affect application behavior (e.g., default configurations, function calls), an attacker may execute arbitrary code or manipulate application logic.
Mitigation
The issue was fixed in commit 98daf567 by adding ownership checks using Object.prototype.hasOwnProperty before access or assignment [3][4]. Users must upgrade to version 1.0.2 or later. No known workarounds exist [2].
- GitHub - clickbar/dot-diver: A lightweight, powerful, and dependency-free TypeScript utility library that provides types and functions to work with object paths in dot notation.
- NVD - CVE-2023-45827
- Merge pull request #12 from clickbar/fix/prototype-pollution · clickbar/dot-diver@98daf56
- Add guards against prototype pollution · clickbar/dot-diver@9790834
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@clickbar/dot-divernpm | < 1.0.2 | 1.0.2 |
Affected products
3Patches
298daf567390dMerge pull request #12 from clickbar/fix/prototype-pollution
3 files changed · +90 −13
CHANGELOG.md+1 −0 modified@@ -7,6 +7,7 @@ All notable changes to **dot-diver** will be documented here. Inspired by [keep - Updated dependencies - Formatted code with new lint rules - Fixed testcase for new TypeScript behavior +- Added guards against prototype pollution, thanks to @d3ng03 (<https://github.com/clickbar/dot-diver/security/advisories/GHSA-9w5f-mw3p-pj47>) ## [1.0.1](https://github.com/clickbar/dot-diver/tree/1.0.1) (2023-03-26)
src/index.ts+34 −13 modified@@ -218,8 +218,12 @@ type PathValueEntry<T, P extends PathEntry<T, Depth>, Depth extends number = 10> type SearchableObject = Record<never, never> | unknown[] +// eslint-disable-next-line @typescript-eslint/unbound-method +const hasOwnProperty = Object.prototype.hasOwnProperty + /** - * Retrives a value from an object by dot notation + * Retrieves a value from an object by dot notation. The value is received by optional chaining, + * therefore this function returns undefined if an intermediate property is undefined. * * @param object - object to get value from * @param path - path to value @@ -236,16 +240,27 @@ function getByPath<T extends SearchableObject, P extends PathEntry<T> & string>( ): PathValueEntry<T, P> { const pathArray = (path as string).split('.') - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - return pathArray.reduce((accumulator: any, current) => accumulator?.[current], object) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return pathArray.reduce((current: any, pathPart) => { + if (typeof current !== 'object' || !hasOwnProperty.call(current, pathPart)) { + return undefined + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return + return current?.[pathPart] + }, object) } /** - * Sets a value in an object by dot notation + * Sets a value in an object by dot notation. If an intermediate property is undefined, + * this function will throw an error. + * * @param object - object to set value in * @param path - path to value * @param value - value to set * + * @throws {Error} - if an intermediate property is undefined + * * @privateRemarks * The intersection between PathEntry<T, 10> and string is necessary for TypeScript to successfully narrow down the type of P based on the user-provided path input. * Without the intersection, the path would just be of type PathEntry<T, 10> and PathValueEntry would be a union of all possible return types. @@ -264,18 +279,24 @@ function setByPath< } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const objectToSet = pathArray.reduce( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - (accumulator: any, current) => accumulator?.[current], - object, - ) + const parentObject = pathArray.reduce((current: any, pathPart) => { + if (typeof current !== 'object' || !hasOwnProperty.call(current, pathPart)) { + throw new Error(`Property ${pathPart} is undefined`) + } - if (objectToSet === undefined) { - throw new Error('Path is invalid') - } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const next = current?.[pathPart] + + if (next === undefined || next === null) { + throw new Error(`Property ${pathPart} is undefined`) + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return next + }, object) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - objectToSet[lastKey] = value + parentObject[lastKey] = value } export type { PathEntry as Path, PathValueEntry as PathValue, SearchableObject }
test/index.test.ts+55 −0 modified@@ -160,3 +160,58 @@ it('Test readme usage example: ⚙️ Customizing the Depth Limit', () => { setByPathDepth5(object, 'f.1.g', 'new array-item-2') expect(object.f[1].g).toBe('new array-item-2') }) + +it('Test for prototype pollution', () => { + const object = {} + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object, '__proto__.polluted', true) + }).toThrowError('__proto__') + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object, '__proto__')).toBe(undefined) + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object, 'constructor.polluted', true) + }).toThrowError('constructor') + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object, 'constructor')).toBe(undefined) + + // @ts-expect-error - this is should not be defined on the object + expect(object.polluted).toBe(undefined) + + const object2 = { constructor: { prototype: { polluted: true } } } + + expect(getByPath(object2, 'constructor.prototype.polluted')).toBe(true) + + setByPath(object2, 'constructor.prototype.polluted', false) + + expect(object2.constructor.prototype.polluted).toBe(false) + + // eslint-disable-next-line @typescript-eslint/no-extraneous-class + const testClass = class TestClass { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function + constructor() {} + } + + const object3 = new testClass() + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object3, 'constructor.prototype')).toBe(undefined) + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object3, 'constructor')).toBe(undefined) + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object3, 'constructor.polluted', true) + }).toThrowError('constructor') + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object3, '__proto__.polluted', true) + }).toThrowError('__proto__') +})
9790834cf4c2Add guards against prototype pollution
3 files changed · +90 −13
CHANGELOG.md+1 −0 modified@@ -7,6 +7,7 @@ All notable changes to **dot-diver** will be documented here. Inspired by [keep - Updated dependencies - Formatted code with new lint rules - Fixed testcase for new TypeScript behavior +- Added guards against prototype pollution, thanks to @d3ng03 (<https://github.com/clickbar/dot-diver/security/advisories/GHSA-9w5f-mw3p-pj47>) ## [1.0.1](https://github.com/clickbar/dot-diver/tree/1.0.1) (2023-03-26)
src/index.ts+34 −13 modified@@ -218,8 +218,12 @@ type PathValueEntry<T, P extends PathEntry<T, Depth>, Depth extends number = 10> type SearchableObject = Record<never, never> | unknown[] +// eslint-disable-next-line @typescript-eslint/unbound-method +const hasOwnProperty = Object.prototype.hasOwnProperty + /** - * Retrives a value from an object by dot notation + * Retrives a value from an object by dot notation. The value is received by optional chaining, + * therefore this function returns undefined if a intermediate property is undefined. * * @param object - object to get value from * @param path - path to value @@ -236,16 +240,27 @@ function getByPath<T extends SearchableObject, P extends PathEntry<T> & string>( ): PathValueEntry<T, P> { const pathArray = (path as string).split('.') - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - return pathArray.reduce((accumulator: any, current) => accumulator?.[current], object) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return pathArray.reduce((current: any, pathPart) => { + if (typeof current !== 'object' || !hasOwnProperty.call(current, pathPart)) { + return undefined + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return + return current?.[pathPart] + }, object) } /** - * Sets a value in an object by dot notation + * Sets a value in an object by dot notation. If a intermediate property is undefined, + * this function will throw an error. + * * @param object - object to set value in * @param path - path to value * @param value - value to set * + * @throws {Error} - if a intermediate property is undefined + * * @privateRemarks * The intersection between PathEntry<T, 10> and string is necessary for TypeScript to successfully narrow down the type of P based on the user-provided path input. * Without the intersection, the path would just be of type PathEntry<T, 10> and PathValueEntry would be a union of all possible return types. @@ -264,18 +279,24 @@ function setByPath< } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const objectToSet = pathArray.reduce( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - (accumulator: any, current) => accumulator?.[current], - object, - ) + const parentObject = pathArray.reduce((current: any, pathPart) => { + if (typeof current !== 'object' || !hasOwnProperty.call(current, pathPart)) { + throw new Error(`Property ${pathPart} is undefined`) + } - if (objectToSet === undefined) { - throw new Error('Path is invalid') - } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const next = current?.[pathPart] + + if (next === undefined || next === null) { + throw new Error(`Property ${pathPart} is undefined`) + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return next + }, object) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - objectToSet[lastKey] = value + parentObject[lastKey] = value } export type { PathEntry as Path, PathValueEntry as PathValue, SearchableObject }
test/index.test.ts+55 −0 modified@@ -160,3 +160,58 @@ it('Test readme usage example: ⚙️ Customizing the Depth Limit', () => { setByPathDepth5(object, 'f.1.g', 'new array-item-2') expect(object.f[1].g).toBe('new array-item-2') }) + +it('Test for prototype pollution', () => { + const object = {} + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object, '__proto__.polluted', true) + }).toThrowError('__proto__') + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object, '__proto__')).toBe(undefined) + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object, 'constructor.polluted', true) + }).toThrowError('constructor') + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object, 'constructor')).toBe(undefined) + + // @ts-expect-error - this is should not be defined on the object + expect(object.polluted).toBe(undefined) + + const object2 = { constructor: { prototype: { polluted: true } } } + + expect(getByPath(object2, 'constructor.prototype.polluted')).toBe(true) + + setByPath(object2, 'constructor.prototype.polluted', false) + + expect(object2.constructor.prototype.polluted).toBe(false) + + // eslint-disable-next-line @typescript-eslint/no-extraneous-class + const testClass = class TestClass { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function + constructor() {} + } + + const object3 = new testClass() + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object3, 'constructor.prototype')).toBe(undefined) + + // @ts-expect-error - this is not a valid path for the object + expect(getByPath(object3, 'constructor')).toBe(undefined) + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object3, 'constructor.polluted', true) + }).toThrowError('constructor') + + expect(() => { + // @ts-expect-error - this is not a valid path for the object + setByPath(object3, '__proto__.polluted', true) + }).toThrowError('__proto__') +})
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-9w5f-mw3p-pj47ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-45827ghsaADVISORY
- github.com/clickbar/dot-diver/commit/9790834cf4c2bca75db00e588e58056dacaf602fghsaWEB
- github.com/clickbar/dot-diver/commit/98daf567390d816fd378ec998eefe2e97f293d5aghsax_refsource_MISCWEB
- github.com/clickbar/dot-diver/security/advisories/GHSA-9w5f-mw3p-pj47ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.