@yemi33/minions 0.1.1949 → 0.1.1950

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-14T23:08:33.100Z"
4
+ "cachedAt": "2026-05-15T00:20:53.688Z"
5
5
  }
package/engine/shared.js CHANGED
@@ -2335,6 +2335,11 @@ function sanitizePath(file, baseDir) {
2335
2335
 
2336
2336
  const _DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
2337
2337
 
2338
+ // DoS caps for the recursive walk. Reaching either limit is treated as
2339
+ // "dangerous" — see hasDangerousKey() for rationale (P-e8b1d3a6 / F6).
2340
+ const HAS_DANGEROUS_KEY_MAX_DEPTH = 64;
2341
+ const HAS_DANGEROUS_KEY_MAX_NODES = 10000;
2342
+
2338
2343
  /**
2339
2344
  * Detect the presence of prototype-pollution attack keys in a JSON-decoded payload.
2340
2345
  *
@@ -2345,43 +2350,55 @@ const _DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
2345
2350
  * but downstream code that shallow-merges the payload into a target object
2346
2351
  * CAN elevate it into a prototype write.
2347
2352
  *
2348
- * Contract is **rejection, not sanitization**: we inspect the top level plus
2349
- * one level deep and return a boolean. Deeper walks are intentionally skipped
2350
- * to avoid their own DoS pathologies on adversarial inputs.
2353
+ * Contract is **rejection, not sanitization**: we walk the full tree and
2354
+ * return a boolean. To avoid DoS via deeply-nested or pathologically-wide
2355
+ * inputs (stack exhaustion, unbounded CPU), the walk is capped:
2356
+ *
2357
+ * - Recursion depth > {@link HAS_DANGEROUS_KEY_MAX_DEPTH} (64) → return true.
2358
+ * - Total visited nodes > {@link HAS_DANGEROUS_KEY_MAX_NODES} (10000) → return true.
2359
+ * Every recursive call counts (including primitive leaves and array elements),
2360
+ * so a pathologically wide payload trips the cap even if no key is dangerous.
2361
+ *
2362
+ * Returning `true` on overflow is the safe-by-default policy: the sole
2363
+ * caller (dashboard.js request-body guard) rejects on `true`, so degrading
2364
+ * to rejection is conservative. The caps also protect against cyclic
2365
+ * objects, since each visit increments the node counter.
2351
2366
  *
2352
2367
  * - Null / undefined / primitives → false.
2353
2368
  * - Arrays are transparent: each element is checked at the same depth as the
2354
- * array itself (an array does NOT consume a depth level).
2355
- * - Max object nesting inspected: 1. Dangerous keys at object-depth 2+
2356
- * are intentionally NOT flagged.
2369
+ * array itself (an array does NOT consume a depth level), but each element
2370
+ * visit increments the node counter.
2357
2371
  * - Never mutates the input.
2358
2372
  *
2359
2373
  * @param {*} obj - any JSON-decoded value
2360
2374
  * @param {number} [_depth=0] - internal recursion counter; do not pass externally
2361
- * @returns {boolean} true if any forbidden key is present at object-depth ≤ 1
2375
+ * @param {{n:number}} [_nodeCount={n:0}] - internal mutable node counter; do not pass externally
2376
+ * @returns {boolean} true if any forbidden key is present, or if the depth/node cap is exceeded
2362
2377
  */
2363
- function hasDangerousKey(obj, _depth = 0) {
2378
+ function hasDangerousKey(obj, _depth = 0, _nodeCount = { n: 0 }) {
2379
+ // DoS caps: count EVERY visited node (including primitive leaves and
2380
+ // array elements per F6 contract), and bail conservatively on either cap.
2381
+ // Returning `true` on overflow is the safe-by-default policy since the
2382
+ // sole caller (dashboard.js request-body guard) rejects on `true`.
2383
+ if (++_nodeCount.n > HAS_DANGEROUS_KEY_MAX_NODES) return true;
2384
+ if (_depth > HAS_DANGEROUS_KEY_MAX_DEPTH) return true;
2385
+
2364
2386
  if (obj === null || obj === undefined || typeof obj !== 'object') return false;
2365
2387
 
2366
2388
  // Arrays are transparent — preserve depth when recursing into elements.
2367
2389
  if (Array.isArray(obj)) {
2368
2390
  for (const elt of obj) {
2369
- if (hasDangerousKey(elt, _depth)) return true;
2391
+ if (hasDangerousKey(elt, _depth, _nodeCount)) return true;
2370
2392
  }
2371
2393
  return false;
2372
2394
  }
2373
2395
 
2374
- // Object: check own keys at the current depth.
2396
+ // Object: check own keys at the current depth, then recurse into values.
2375
2397
  for (const key of Object.keys(obj)) {
2376
2398
  if (_DANGEROUS_KEYS.has(key)) return true;
2377
2399
  }
2378
-
2379
- // Stop after one level of object nesting. Deeper recursion is an explicit
2380
- // non-goal (see DoS note in the header).
2381
- if (_depth >= 1) return false;
2382
-
2383
2400
  for (const v of Object.values(obj)) {
2384
- if (hasDangerousKey(v, _depth + 1)) return true;
2401
+ if (hasDangerousKey(v, _depth + 1, _nodeCount)) return true;
2385
2402
  }
2386
2403
  return false;
2387
2404
  }
@@ -3822,6 +3839,8 @@ module.exports = {
3822
3839
  isAllowedOrigin,
3823
3840
  buildSecurityHeaders,
3824
3841
  hasDangerousKey,
3842
+ HAS_DANGEROUS_KEY_MAX_DEPTH,
3843
+ HAS_DANGEROUS_KEY_MAX_NODES,
3825
3844
  validateProjectName,
3826
3845
  validateProjectPath,
3827
3846
  validatePid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1949",
3
+ "version": "0.1.1950",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"