Add new object utility helpers and harden defaults against prototype pollution#564
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #564 +/- ##
==========================================
- Coverage 99.43% 99.39% -0.05%
==========================================
Files 150 154 +4
Lines 4460 4659 +199
Branches 951 998 +47
==========================================
+ Hits 4435 4631 +196
- Misses 25 28 +3
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds new object utility helpers to the TypeScript utility library, along with tests, documentation updates, exports, and adjusted bundle/dependency metadata.
Changes:
- Adds object helpers for picking/omitting, predicate filtering, mapping values, defaults/conditional merge, and shallow diffs.
- Adds common object tests and exports the new helpers from the main entrypoint.
- Updates README/backlog docs, dependency minimums, size limits, and Copilot instructions.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
lib/src/object/pick.ts |
Adds pick/omit and predicate-based object selection helpers. |
lib/src/object/map_values.ts |
Adds value-mapping helper for object properties. |
lib/src/object/defaults.ts |
Adds conditional merge and defaults helpers. |
lib/src/object/diff.ts |
Adds shallow object diff helper. |
lib/src/index.ts |
Exports the new object helpers from the main entrypoint. |
lib/test/src/common/object/pick.test.ts |
Adds tests for pick/omit helpers. |
lib/test/src/common/object/map_values.test.ts |
Adds tests for object value mapping. |
lib/test/src/common/object/defaults.test.ts |
Adds tests for merge/defaults helpers. |
lib/test/src/common/object/diff.test.ts |
Adds tests for shallow diff behavior. |
README.md |
Documents the new object utilities in feature summaries and API matrix. |
docs/feature-backlog.md |
Removes implemented object utilities from the backlog. |
package.json |
Raises selected development dependency minimums. |
.size-limit.json |
Raises several bundle size thresholds. |
.github/copilot-instructions.md |
Adds Azure-specific Copilot instruction rules. |
Comments suppressed due to low confidence (2)
lib/src/object/pick.ts:77
- The public overload above accepts any string keys, but the implementation signature only accepts
ReadonlyArray<string>while the typed overload can passkeyof Tkeys, including symbol/number keys. This mismatch makes the overload set incompatible with the implementation and can break TypeScript compilation.
export function objOmit<T>(source: T, keys: ReadonlyArray<string>): Partial<T> {
lib/src/object/diff.ts:50
- Added properties whose value is
undefinedare dropped because a missing base key is converted toundefinedand then compared with!==. This contradicts the documented behavior that properties present inmodifiedbut not inbaseare included;{}vs{ a: undefined }should retain theakey.
const baseVal = objHasOwn(base, key) ? (base as any)[key] : undefined;
if (baseVal !== value) {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (3)
lib/src/object/diff.ts:53
objDiffdoes not include newly added properties whose value isundefined: whenbaselacks the key,baseValis set toundefined, sobaseVal !== valueis false. This contradicts the documented behavior that properties present inmodifiedbut not inbaseare included.
const baseVal = objHasOwn(base, key) ? (base as any)[key] : undefined;
if (baseVal !== value) {
lib/src/object/pick.ts:123
objPickBycannot include enumerable symbol properties becauseobjForEachKeyonly iterates string keys, but the function returnsPartial<T>and documents filtering own enumerable properties. A source with symbol-keyed data will be silently missing those properties from the result.
objForEachKey(source, (key, value) => {
if (predicate(key, value)) {
(result as any)[key] = value;
lib/src/object/pick.ts:153
objOmitBydrops enumerable symbol properties regardless of the predicate becauseobjForEachKeyonly visits string keys. Since the result is typed asPartial<T>and the docs describe own enumerable properties, this can silently lose symbol-keyed data.
objForEachKey(source, (key, value) => {
if (!predicate(key, value)) {
(result as any)[key] = value;
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
nevware21-bot
left a comment
There was a problem hiding this comment.
Approved by nevware21-bot
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (7)
lib/test/src/common/object/defaults.test.ts:215
- This test does not create an own enumerable
__proto__property because"__proto__"in an object literal is interpreted as prototype syntax. As written, it would pass even ifobjDefaultscopied an own__proto__key from untrusted input, so the key prototype-pollution path is not covered.
it("should ignore unsafe source keys", () => {
const target: any = {};
objDefaults(target, { "__proto__": { polluted: true }, constructor: "bad", prototype: "bad", safe: 1 } as any);
assert.equal(target.safe, 1);
assert.isFalse(Object.prototype.hasOwnProperty.call(target, "__proto__"));
lib/src/object/defaults.ts:66
- The docs describe copying own enumerable source properties but do not mention that unsafe keys are filtered and unsafe built-in prototype targets are left unchanged. Since this helper intentionally skips keys such as
constructorandprototype, the generated API docs should make that behavior explicit.
* This is similar to Lodash `_.defaults()`, but it only considers each source object's own
* enumerable properties and does not copy inherited source properties.
* @since 0.14.0
* @group Object
lib/test/src/common/object/for_each_key.test.ts:223
- The
__proto__entry in this fixture is not an own enumerable property, so this case only verifiesconstructor/prototypefiltering. It does not cover the own-__proto__path thatobjForEachKeySafeis supposed to block.
const obj = Object.assign({}, {
"__proto__": "attack",
"constructor": "attack",
"prototype": "attack",
"name": "Dave"
lib/test/src/common/object/for_each_key.test.ts:317
- This
__proto__entry is prototype-literal syntax rather than an own enumerable key, so the later assertion does not prove thatobjForEachKeySafefilters own__proto__properties on complex inputs.
func: function() {
return "test";
},
"__proto__": "attack"
});
lib/src/object/forEachOwnKey.ts:61
- This new exported helper has no
@exampleblock after replacing the old documented helper, so the generated API docs lose the usage example for safe iteration.
* @typeParam T - The source type.
* @param theObject - The object-like value to iterate.
* @param callbackfn - Invoked for each safe own enumerable key.
* @param thisArg - [Optional] The `this` context for the callback.
*/
lib/src/object/pick.ts:79
- The exposed overloads still lack a
ReadonlyArray<PropertyKey>fallback, so callers with a dynamic list of string/number/symbol keys cannot use the implementation's widenedPropertyKeysupport without casts. The implementation signature itself is not part of overload resolution for consumers.
export function objOmit<T, const K extends keyof T>(source: T, keys: ReadonlyArray<K>): Omit<T, K>;
/*#__NO_SIDE_EFFECTS__*/
export function objOmit<T>(source: T, keys: ReadonlyArray<string>): Partial<T>;
export function objOmit<T>(source: T, keys: ReadonlyArray<PropertyKey>): Partial<T> {
lib/src/object/defaults.ts:90
- The unsafe-target guard is incomplete for this hardening path: built-in prototypes such as
ArrayBuffer.prototypeand typed-array/DataView prototypes are not recognized byisUnsafeTarget, soobjDefaultscan still mutate them when used astarget.
if (target && !isUnsafeTarget(target)) {
arrForEach(sources, (source) => {
if (source) {
forEachOwnKeySafe(source, (key, value) => {
if (!objHasOwn(target, key) || isStrictUndefined((target as any)[key])) {
…pollution - add new object helpers: objPick, objOmit, objPickBy, objOmitBy, objMapValues, objMergeIf, objDefaults, and objDiff - export the new helpers from the public index - add common test coverage for pick/omit, mapValues, defaults/mergeIf, and diff behavior - fix objPick/objOmit overload compatibility by widening implementation signatures to PropertyKey - preserve numeric key omission at runtime in objOmit when typed overloads pass numeric keys - document objDefaults as similar to Lodash defaults while clarifying it only uses own enumerable source properties - harden objMergeIf and objDefaults against prototype pollution by skipping unsafe keys and blocking unsafe built-in prototype targets
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (2)
lib/src/object/forEachOwnKey.ts:143
forEachOwnKeySafenow delegates toforEachOwnKey, which readstheObject[key]before this filter runs. That means an unsafe own enumerable key such as__proto__with a getter is still invoked even though the key is skipped, whereas this safe helper should avoid touching unsafe-key values before filtering to prevent side effects from untrusted input.
forEachOwnKey(theObject, (key, value) => {
if (!isUnsafePropKey(key)) {
return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
lib/src/object/for_each_key.ts:114
- This safe wrapper receives
valuefromobjForEachKeyafterobjForEachKeyhas already evaluatedtheObject[key], so an unsafe own key like__proto__with a getter is invoked beforeisUnsafePropKeycan skip it. Filter the key before reading the property value so the safe variant does not execute accessors on keys it promises to ignore.
objForEachKey(theObject, (key: string, value: T[keyof T]) => {
if (!isUnsafePropKey(key)) {
return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
nevware21-bot
left a comment
There was a problem hiding this comment.
Approved by nevware21-bot
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
lib/test/src/common/object/for_each_key.test.ts:355
- This test also uses
Object.assignwith a "proto" key. Because__proto__can be treated specially during assignment, the resulting object may not actually contain an own enumerable "proto" property, so it doesn’t reliably validate thatobjForEachKeySafefilters it out. Prefer defining "proto" withObject.definePropertyso the test deterministically exercises the unsafe-key filtering logic.
it("should work with complex objects and filter unsafe keys", () => {
const complexObj = Object.assign({}, {
num: 42,
str: "hello",
bool: true,
arr: [1, 2, 3],
nested: { a: 1, b: 2 },
func: function() {
return "test";
},
"__proto__": "attack"
});
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lib/src/object/pick.ts:87
objOmitis typed to acceptArrayLike<...>keys, but the function currently no-ops unlessisArray(keys)is true. That means non-Array array-likes (while still assignable toArrayLike<PropertyKey>) won’t omit anything and will behave inconsistently with the declared overloads. Consider accepting array-like keys (and rejecting string inputs explicitly) or narrowing the types to real arrays to match the runtime check.
export function objOmit<T>(source: T, keys: ArrayLike<PropertyKey>): Partial<T> {
const result: Partial<T> = objCreate(null);
if (source && isArray(keys)) {
forEachOwnKey(source, (key, value) => {
let hasKey = arrIndexOf(keys, key) !== -1;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
lib/test/src/common/object/for_each_key.test.ts:356
Object.assign({}, { "__proto__": ... })is used here to inject an unsafe key, but__proto__is special-cased and may not become an own enumerable property (and can alter the target’s prototype), so this is not a reliable test for filtering behavior. Define "proto" viaObject.definePropertybefore running the assertions.
it("should work with complex objects and filter unsafe keys", () => {
const complexObj = Object.assign({}, {
num: 42,
str: "hello",
bool: true,
arr: [1, 2, 3],
nested: { a: 1, b: 2 },
func: function() {
return "test";
},
"__proto__": "attack"
});
| * ```ts | ||
| * // Safe iteration - unsafe keys are skipped | ||
| * const untrustedObj = Object.assign({}, { "__proto__": "attack", "name": "Alice", "constructor": {} }); | ||
| * objForEachKeySafe(untrustedObj, (key, value) => { | ||
| * console.log(key, value); // Only prints: name Alice | ||
| * }); | ||
| * ``` |
…port in defaults - Add `forEachOwnKey`: iterates own enumerable string + symbol (PropertyKey) keys without unsafe-key filtering - Add `objForEachKeySafe`: safe wrapper around `objForEachKey` that filters unsafe keys (__proto__, constructor, prototype) before invoking the callback - Refactor `forEachOwnKeySafe` (previously in forEachOwnKeySafe.ts) to use `forEachOwnKey` as its base, renamed module to forEachOwnKey.ts - Fix `objDefaults` and `objMergeIf` to process enumerable symbol-keyed properties (previously silently skipped) - Update callback types in `defaults.ts` to accept PropertyKey to properly support symbol keys - Update `objForEachKey` TSDoc to document string-key-only behaviour and cross-reference all four iteration helpers - Add tests: symbol enumerable/non-enumerable defaults, objForEachKeySafe unsafe-key filtering, forEachOwnKey symbol iteration - Update README Available Utilities table: add forEachOwnKey, forEachOwnKeySafe, objForEachKeySafe entries
nevware21-bot
left a comment
There was a problem hiding this comment.
Approved by nevware21-bot
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
nevware21-bot
left a comment
There was a problem hiding this comment.
Approved by nevware21-bot
## Release v0.14.0 This PR increases the version to `0.14.0` and updates the changelog with all significant changes since v0.13.0. ### Summary of Changes #### Features - New array helpers and array-like detection (#525) - `strReplace` and `strReplaceAll` string helpers (#527) - `strCapitalizeWords` helper (#528) - `strTruncate`, `strCount`, `strAt`, `strMatchAll` helpers (#529, #530) - `arrFlatMap` with ES5 polyfill (#533) - Typing utilities and expanded TSDoc examples (#535) - `isAsyncIterable` and `isIntegerInRange` helpers (#536) - `strStartsWithAny`, `strEndsWithAny`, `strWrap`, `strUnwrap`, `strNormalizeNewlines` (#543) - New object utility helpers and prototype pollution hardening (#564, #565) #### Bug Fixes - Fix ES2015 built-in type errors in consumers by adding lib reference directive to published declarations (#558) - Fix `thisArg` binding in `polyArrFindIndex` / `polyArrFindLastIndex` polyfills (#562) - Fix falsy `thisArg` (0, `''`, `false`) being overridden in `arrForEach`, `iterForOf`, `objForEachKey` (#566) #### Repository Improvements - Drop Node.js 16 from CI matrix and add Node.js 24 (#549) - Upgrade Grunt devDependency to v1.6.2 (#552) - Add funding metadata to published package manifests (#554) ### Files Updated - `CHANGELOG.md` — Added v0.14.0 entry - `package.json` — Version bumped to 0.14.0 - `lib/package.json` — Version bumped to 0.14.0 - `README.md` — Updated recommended version specification
Uh oh!
There was an error while loading. Please reload this page.