-
-
Notifications
You must be signed in to change notification settings - Fork 2
Add new object utility helpers and harden defaults against prototype pollution #564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /* | ||
| * @nevware21/ts-utils | ||
| * https://github.com/nevware21/ts-utils | ||
| * | ||
| * Copyright (c) 2026 NevWare21 Solutions LLC | ||
| * Licensed under the MIT license. | ||
| */ | ||
|
|
||
| import { arrForEach } from "../array/forEach"; | ||
| import { isStrictUndefined } from "../helpers/base"; | ||
| import { forEachOwnKeySafe } from "./forEachOwnKey"; | ||
| import { isUnsafeTarget } from "./isUnsafeTarget"; | ||
|
|
||
| /** | ||
| * Copies own enumerable properties from `source` to `target` **only** when the predicate | ||
| * returns truthy for that key/value pair. All other own properties on `target` are left | ||
| * unchanged. | ||
| * | ||
| * Security behavior: this helper iterates source keys via {@link forEachOwnKeySafe}, so unsafe | ||
| * keys (`__proto__`, `constructor`, `prototype`) are ignored even if the predicate returns true. | ||
| * It also skips all writes when `target` is a guarded built-in prototype object per | ||
| * {@link isUnsafeTarget}. | ||
| * @since 0.14.0 | ||
| * @group Object | ||
| * @typeParam T - The type of the target object | ||
| * @param target - The target object to merge into | ||
| * @param source - The source object to merge from. Null or undefined source is safely ignored. | ||
| * @param predicate - A function `(key, srcValue, tgtValue) => boolean` called for each own | ||
| * enumerable safe property key of `source` (string and symbol). The property is merged only | ||
| * when the predicate returns truthy. | ||
| * @returns The `target` object (mutated in place). | ||
|
nev21 marked this conversation as resolved.
|
||
| * @example | ||
| * ```ts | ||
| * const target = { a: 1, b: 2 }; | ||
| * const source = { b: 99, c: 3 }; | ||
| * | ||
| * // Only merge keys whose source value is greater than 2 | ||
| * objMergeIf(target, source, (key, srcVal) => srcVal > 2); | ||
| * // target => { a: 1, b: 99, c: 3 } | ||
| * | ||
| * // Only merge when the key does not already exist in target | ||
| * const t2 = { x: 10 }; | ||
| * objMergeIf(t2, { x: 99, y: 5 }, (key, _sv, tgtVal) => tgtVal === undefined); | ||
| * // t2 => { x: 10, y: 5 } | ||
| * | ||
| * // Unsafe keys are filtered even if predicate returns true | ||
| * const t3: any = {}; | ||
| * objMergeIf(t3, { constructor: "ignored", safe: 1 } as any, () => true); | ||
| * // t3 => { safe: 1 } | ||
| * ``` | ||
| */ | ||
| export function objMergeIf<T>( | ||
| target: T, | ||
| source: Record<PropertyKey, any> | null | undefined, | ||
| predicate: (key: PropertyKey, srcValue: any, tgtValue: any) => boolean | ||
| ): T { | ||
| if (target && source && !isUnsafeTarget(target)) { | ||
| forEachOwnKeySafe(source, (key, value) => { | ||
| if (predicate(key, value, (target as any)[key])) { | ||
| (target as any)[key] = value; | ||
|
nev21 marked this conversation as resolved.
|
||
| } | ||
| }); | ||
| } | ||
| return target; | ||
| } | ||
|
|
||
| /** | ||
| * Assigns own enumerable properties from one or more `sources` onto `target` **only** for | ||
| * properties that are currently `undefined` on `target` — it never overwrites an already-defined | ||
| * value (including `null`). Sources are processed left-to-right; the first defined value wins. | ||
| * | ||
| * This is similar to Lodash `_.defaults()`, but it only considers each source object's own | ||
|
nev21 marked this conversation as resolved.
|
||
| * enumerable properties and does not copy inherited source properties. | ||
| * | ||
| * **Security filtering:** to guard against prototype-pollution attacks this function applies two | ||
| * layers of protection: | ||
| * - **Unsafe source keys** (`__proto__`, `constructor`, `prototype`) are silently skipped and | ||
| * never written to `target`, even when those keys exist as own enumerable properties of a source. | ||
| * - **Guarded targets** (built-in prototype objects such as `Object.prototype`, | ||
| * `Array.prototype`, etc.) are rejected entirely — `target` is returned unchanged. | ||
| * | ||
| * This means that calls like `objDefaults(obj, { constructor: fn })` or | ||
| * `objDefaults(Object.prototype, ...)` are silently no-ops for the filtered keys / guarded target. | ||
| * @since 0.14.0 | ||
| * @group Object | ||
| * @typeParam T - The type of the target object | ||
| * @param target - The destination object. Modified in place. | ||
| * @param sources - One or more source objects. Null / undefined sources are skipped. | ||
| * @returns The `target` object with all defaults applied. | ||
| * @example | ||
| * ```ts | ||
| * const options = { timeout: 5000 }; | ||
| * const defaults = { timeout: 3000, retries: 3, verbose: false }; | ||
| * | ||
| * objDefaults(options, defaults); | ||
| * // => { timeout: 5000, retries: 3, verbose: false } | ||
| * // `timeout` was kept because it was already defined. | ||
| * | ||
| * // Multiple sources — first defined value wins | ||
| * objDefaults({}, { a: 1 }, { a: 99, b: 2 }); | ||
| * // => { a: 1, b: 2 } | ||
| * | ||
| * // Unsafe source keys are silently skipped (prototype-pollution guard) | ||
| * const cfg: any = { host: "localhost" }; | ||
| * const src: any = { host: "evil.com", __proto__: { admin: true }, constructor: String }; | ||
| * objDefaults(cfg, src); | ||
| * // => { host: "localhost" } — __proto__ and constructor were never written | ||
| * ``` | ||
| */ | ||
| export function objDefaults<T>(target: T, ...sources: Array<Partial<T> | null | undefined>): T { | ||
| if (target && !isUnsafeTarget(target)) { | ||
| arrForEach(sources, (source) => { | ||
| if (source) { | ||
| forEachOwnKeySafe(source, (key, value) => { | ||
| if (isStrictUndefined((target as any)[key])) { | ||
| (target as any)[key] = value; | ||
|
nev21 marked this conversation as resolved.
nev21 marked this conversation as resolved.
|
||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| return target; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /* | ||
| * @nevware21/ts-utils | ||
| * https://github.com/nevware21/ts-utils | ||
| * | ||
| * Copyright (c) 2026 NevWare21 Solutions LLC | ||
| * Licensed under the MIT license. | ||
| */ | ||
|
|
||
| import { isStrictNullOrUndefined } from "../helpers/base"; | ||
| import { objCreate } from "./create"; | ||
| import { forEachOwnKey } from "./forEachOwnKey"; | ||
| import { objHasOwn } from "./has_own"; | ||
|
|
||
| /** | ||
| * Returns a shallow diff of two objects: a new object containing only the own enumerable | ||
| * properties from `modified` whose values differ from the corresponding values in `base` | ||
| * (using strict equality `!==`). Properties present in `modified` but not in `base` are | ||
| * also included. Properties removed in `modified` (i.e. present only in `base`) are | ||
| * **not** included — use the result to describe *what changed* in `modified`. | ||
| * @since 0.14.0 | ||
| * @group Object | ||
| * @typeParam T - The type of the base object | ||
| * @typeParam U - The type of the modified object (defaults to `Partial<T>`) | ||
| * @param base - The original / reference object | ||
| * @param modified - The updated object to compare against `base` | ||
| * @returns A new plain object with the keys from `modified` that differ from `base`. | ||
| * Returns an empty object when the two objects are equivalent or when either argument | ||
| * is null or undefined. | ||
| * @example | ||
| * ```ts | ||
| * const prev = { x: 1, y: 2, z: 3 }; | ||
| * const next = { x: 1, y: 99, z: 3 }; | ||
| * | ||
| * objDiff(prev, next); // { y: 99 } | ||
| * | ||
| * // Added keys are included | ||
| * objDiff({ a: 1 }, { a: 1, b: 2 }); // { b: 2 } | ||
| * | ||
| * // Removed keys are NOT included | ||
| * objDiff({ a: 1, b: 2 }, { a: 1 }); // {} | ||
| * | ||
| * // null / undefined values are compared strictly | ||
| * objDiff({ a: null }, { a: undefined }); // { a: undefined } | ||
| * ``` | ||
| */ | ||
| /*#__NO_SIDE_EFFECTS__*/ | ||
| export function objDiff<T, U extends Partial<T> = Partial<T>>(base: T, modified: U): Partial<U> { | ||
| const result: Partial<U> = objCreate(null); | ||
|
nev21 marked this conversation as resolved.
|
||
|
|
||
| if (!isStrictNullOrUndefined(base)) { | ||
| forEachOwnKey(modified, (key, value) => { | ||
| const hasBase = objHasOwn(base, key); | ||
| const baseVal = hasBase ? (base as any)[key] : undefined; | ||
| if (!hasBase || baseVal !== value) { | ||
| (result as any)[key] = value; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.