Skip to content

Commit 26b4766

Browse files
authored
Add prototype-pollution guards and array key helpers (#565)
Add new public helpers for safe own-key iteration and unsafe target/key detection, plus new array key utilities for dense and sparse array-like values. - add forEachOwnKeySafe(), isUnsafePropKey(), and isUnsafeTarget() - update deep copy logic to use safe own-key iteration - add arrKeys() iterator and arrIndexKeys() own-index enumeration - export the new public APIs from the main index - add tests covering sparse arrays, array-like values, and unsafe keys - document the new security helpers in README and docs
1 parent 4d28559 commit 26b4766

28 files changed

Lines changed: 1063 additions & 36 deletions

.size-limit.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,42 @@
22
{
33
"name": "es5-full",
44
"path": "lib/dist/es5/mod/ts-utils.js",
5-
"limit": "29.5 kb",
5+
"limit": "31 kb",
66
"brotli": false,
77
"running": false
88
},
99
{
1010
"name": "es6-full",
1111
"path": "lib/dist/es6/mod/ts-utils.js",
12-
"limit": "28.5 kb",
12+
"limit": "30 kb",
1313
"brotli": false,
1414
"running": false
1515
},
1616
{
1717
"name": "es5-full-brotli",
1818
"path": "lib/dist/es5/mod/ts-utils.js",
19-
"limit": "10.5 kb",
19+
"limit": "11 kb",
2020
"brotli": true,
2121
"running": false
2222
},
2323
{
2424
"name": "es6-full-brotli",
2525
"path": "lib/dist/es6/mod/ts-utils.js",
26-
"limit": "10.5 kb",
26+
"limit": "11 kb",
2727
"brotli": true,
2828
"running": false
2929
},
3030
{
3131
"name": "es5-zip",
3232
"path": "lib/dist/es5/mod/ts-utils.js",
33-
"limit": "11.5 Kb",
33+
"limit": "12 Kb",
3434
"gzip": true,
3535
"running": false
3636
},
3737
{
3838
"name": "es6-zip",
3939
"path": "lib/dist/es6/mod/ts-utils.js",
40-
"limit": "11.5 Kb",
40+
"limit": "12 Kb",
4141
"gzip": true,
4242
"running": false
4343
},

README.md

Lines changed: 9 additions & 4 deletions
Large diffs are not rendered by default.

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ If you find this library useful, please consider [sponsoring @nevware21](https:/
2828

2929
### Advanced Topics
3030
- **[Advanced Deep Copy Functionality](./advanced-deep-copy.md)** - Using custom handlers with deep copy operations
31+
- **[Security Helpers](./security-helpers.md)** - Preventing prototype pollution during object copy and merge operations
3132
- **[Performance and Minification Benefits](./usage-guide.md#performance-and-minification-benefits)** - Understanding how the library optimizes for performance
3233
- **[Timeout Overrides](./timeout-overrides.md)** - Using Timeout overrides for custom scheduling behavior
3334
- **[Bundle Size Optimization](./size-optimization.md)** - Detailed byte-level measurements and strategies for reducing bundle size

docs/security-helpers.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Security Helpers
2+
3+
When copying, merging, or transforming untrusted object-like input, JavaScript applications can accidentally allow prototype pollution via dangerous keys such as `__proto__`, `constructor`, and `prototype`.
4+
5+
This package includes helpers to reduce that risk in common object traversal and assignment flows.
6+
7+
## Available Helpers
8+
9+
- [forEachOwnKeySafe](./typedoc/functions/forEachOwnKeySafe.html) safely iterates only own enumerable keys and skips dangerous property names.
10+
- [isUnsafePropKey](./typedoc/functions/isUnsafePropKey.html) identifies keys commonly used for prototype pollution attacks.
11+
- [isUnsafeTarget](./typedoc/functions/isUnsafeTarget.html) detects built-in prototype objects so you can avoid mutating native prototypes.
12+
13+
## Recommended Usage
14+
15+
Use `isUnsafeTarget()` to guard the destination object, and `forEachOwnKeySafe()` to iterate source keys.
16+
17+
```typescript
18+
import {
19+
forEachOwnKeySafe,
20+
isUnsafeTarget
21+
} from "@nevware21/ts-utils";
22+
23+
function copyTrustedValues(source: any, target: any) {
24+
if (isUnsafeTarget(target)) {
25+
throw new TypeError("Refusing to write to a built-in prototype");
26+
}
27+
28+
forEachOwnKeySafe(source, (key, value) => {
29+
target[key] = value;
30+
});
31+
32+
return target;
33+
}
34+
35+
const payload = {
36+
safe: true,
37+
__proto__: {
38+
polluted: true
39+
}
40+
};
41+
42+
copyTrustedValues(payload, {});
43+
// Result only includes the safe enumerable keys.
44+
```
45+
46+
## Notes
47+
48+
- These helpers provide safer defaults, but they do not replace application-level input validation.
49+
- If you accept untrusted JSON or query-derived objects, validate schema and value types before assignment.

lib/package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,23 @@
148148
"object-keys",
149149
"lightweight",
150150
"performance-optimized",
151-
"type-safe"
151+
"type-safe",
152+
"prototype-pollution",
153+
"prototype-pollution-prevention",
154+
"secure-merge",
155+
"secure-copy",
156+
"object-security",
157+
"defensive-programming",
158+
"forEachOwnKeySafe",
159+
"isUnsafePropKey",
160+
"isUnsafeTarget",
161+
"arrKeys",
162+
"arrIndexKeys",
163+
"sparse-array",
164+
"array-like",
165+
"from-entries",
166+
"has-own",
167+
"base64"
152168
],
153169
"types": "./dist/types/ts-utils.d.ts",
154170
"main": "./dist/es5/main/ts-utils.js",

lib/src/array/arrIndexKeys.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* @nevware21/ts-utils
3+
* https://github.com/nevware21/ts-utils
4+
*
5+
* Copyright (c) 2026 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { getLength } from "../helpers/length";
10+
import { _throwIfNullOrUndefined } from "../internal/throwIf";
11+
import { mathToInt } from "../math/to_int";
12+
import { objHasOwn } from "../object/has_own";
13+
14+
/**
15+
* Returns only present own numeric index keys for an array-like value.
16+
*
17+
* Unlike {@link arrKeys}, this skips holes / missing indexes.
18+
* For example, an array with indexes `0`, `1`, `2`, `10` returns `[0, 1, 2, 10]`.
19+
* @since 0.14.0
20+
* @group Array
21+
* @group ArrayLike
22+
* @param value - The array-like value to enumerate.
23+
* @returns An array containing only present own numeric index keys.
24+
* @example
25+
* ```ts
26+
* arrIndexKeys(["a", "b", "c"]);
27+
* // [0, 1, 2]
28+
*
29+
* const sparse: any[] = [];
30+
* sparse[0] = "a";
31+
* sparse[10] = "z";
32+
* arrIndexKeys(sparse);
33+
* // [0, 10]
34+
* ```
35+
*/
36+
/*#__NO_SIDE_EFFECTS__*/
37+
export function arrIndexKeys<T = any>(value: ArrayLike<T>): number[] {
38+
_throwIfNullOrUndefined(value);
39+
40+
let keys: number[] = [];
41+
let len = mathToInt(getLength(value));
42+
if (len > 0) {
43+
for (let lp = 0; lp < len; lp++) {
44+
if (objHasOwn(value, lp)) {
45+
keys.push(lp);
46+
}
47+
}
48+
}
49+
50+
return keys;
51+
}

lib/src/array/arrKeys.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* @nevware21/ts-utils
3+
* https://github.com/nevware21/ts-utils
4+
*
5+
* Copyright (c) 2026 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { ArrProto } from "../internal/constants";
10+
import { _throwIfNullOrUndefined } from "../internal/throwIf";
11+
import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction";
12+
import { createIterableIterator } from "../iterator/create";
13+
import { mathToInt } from "../math/to_int";
14+
15+
/**
16+
* Returns an iterator over all numeric keys from `0` to `length - 1`.
17+
*
18+
* This uses `Array.prototype.keys()` when available and falls back to {@link polyArrKeys}.
19+
* Unlike {@link arrIndexKeys}, this always iterates all index positions, including holes.
20+
* @since 0.14.0
21+
* @function
22+
* @group Array
23+
* @group ArrayLike
24+
* @group Iterator
25+
* @param value - The array-like value to get key iterator for.
26+
* @returns An iterable iterator of numeric index keys.
27+
* @example
28+
* ```ts
29+
* arrFrom(arrKeys(["a", "b", "c"]));
30+
* // [0, 1, 2]
31+
*
32+
* const sparse: any[] = [];
33+
* sparse[2] = "c";
34+
* arrFrom(arrKeys(sparse));
35+
* // [0, 1, 2]
36+
* ```
37+
*/
38+
export const arrKeys: <T = any>(value: ArrayLike<T>) => IterableIterator<number> = (/*#__PURE__*/_unwrapFunctionWithPoly("keys", ArrProto as any, polyArrKeys) as any);
39+
40+
/**
41+
* Polyfill implementation of `Array.prototype.keys()` for array-like values.
42+
* @since 0.14.0
43+
* @group Array
44+
* @group ArrayLike
45+
* @group Iterator
46+
* @group Polyfill
47+
* @param value - The array-like value to get key iterator for.
48+
* @returns An iterable iterator of numeric index keys.
49+
* @example
50+
* ```ts
51+
* arrFrom(polyArrKeys(["a", "b", "c"]));
52+
* // [0, 1, 2]
53+
*
54+
* arrFrom(polyArrKeys({ length: 3, 0: "a", 2: "c" }));
55+
* // [0, 1, 2]
56+
* ```
57+
*/
58+
/*#__NO_SIDE_EFFECTS__*/
59+
export function polyArrKeys<T = any>(value: ArrayLike<T>): IterableIterator<number> {
60+
_throwIfNullOrUndefined(value);
61+
62+
let idx = -1;
63+
let len = mathToInt(value.length);
64+
if (len < 0) {
65+
len = 0;
66+
}
67+
68+
return createIterableIterator<number>({
69+
n: function() {
70+
idx++;
71+
let isDone = idx >= len;
72+
if (!isDone) {
73+
this.v = idx;
74+
}
75+
76+
return isDone;
77+
}
78+
});
79+
}

lib/src/array/groupBy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88

99
import { isArrayLike, isFunction } from "../helpers/base";
10+
import { objDefine } from "../object/define";
1011
import { objHasOwn } from "../object/has_own";
12+
import { isUnsafePropKey } from "../object/isUnsafePropKey";
1113
import { asString } from "../string/as_string";
1214
import { isSymbol } from "../symbol/symbol";
1315
import { arrForEach } from "./forEach";
@@ -104,7 +106,11 @@ export function arrGroupBy<T>(
104106
const theKey = isSymbol(keyVal) ? keyVal : asString(keyVal);
105107

106108
if (!objHasOwn(result, theKey)) {
107-
result[theKey] = [];
109+
if (isUnsafePropKey(theKey)) {
110+
objDefine(result, theKey, { v: [] });
111+
} else {
112+
result[theKey] = [];
113+
}
108114
}
109115

110116
result[theKey].push(item);

0 commit comments

Comments
 (0)