bank20baht-validator 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/dist/index.js +26 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -443,6 +443,25 @@ function assertValidFields(form) {
443
443
  }
444
444
  }
445
445
  }
446
+ /**
447
+ * True for the textbook ReDoS shape: an unbounded quantifier (`* + {n,}`)
448
+ * applied to a group whose ENTIRE body is one unboundedly-quantified atom —
449
+ * `(a+)+`, `(\d*)*`, `(?:\w+){2,}`. That exact nesting is always either
450
+ * redundant (matches the same language as a single quantifier) or exponential
451
+ * to backtrack, so rejecting it has no legitimate false positives. It is
452
+ * deliberately NARROW: longer bodies like `(abc+)+` are safe (the literal
453
+ * prefix removes the cross-iteration ambiguity) and other catastrophic shapes
454
+ * (e.g. overlapping alternation `(a|a)*`) are out of scope — patterns are
455
+ * builder-authored, so the goal is catching the classic authoring mistake at
456
+ * save time, not sandboxing hostile regexes.
457
+ */
458
+ function hasNestedUnboundedQuantifier(pattern) {
459
+ // Strip escaped chars and character classes first so `\+`, `[+]` etc.
460
+ // aren't mistaken for quantifiers or group delimiters.
461
+ const stripped = pattern.replace(/\\./g, 'x').replace(/\[[^\]]*\]/g, 'C');
462
+ const unbounded = '(?:[*+]|\\{\\d+,\\})';
463
+ return new RegExp(`\\((?:\\?:)?[^()|]${unbounded}\\??\\)${unbounded}`).test(stripped);
464
+ }
446
465
  function assertFieldShape(field, formName) {
447
466
  if (typeof field.type !== 'string' || !FIELD_TYPES.has(field.type)) {
448
467
  throw new Error(`unknown field type ${JSON.stringify(field.type)} on ${formName}`);
@@ -473,12 +492,18 @@ function assertFieldShape(field, formName) {
473
492
  if (v.type === 'pattern') {
474
493
  // compile now — an invalid regex would otherwise throw (or hang, if
475
494
  // catastrophic) inside validateField on every future submission
495
+ const pattern = String(v.value);
476
496
  try {
477
- new RegExp(String(v.value));
497
+ new RegExp(pattern);
478
498
  }
479
499
  catch {
480
500
  throw new Error(`invalid pattern ${JSON.stringify(v.value)} on ${formName}.${field.key}`);
481
501
  }
502
+ if (hasNestedUnboundedQuantifier(pattern)) {
503
+ throw new Error(`pattern on ${formName}.${field.key} nests unbounded quantifiers ` +
504
+ `(e.g. "(a+)+") — this backtracks catastrophically on hostile input; ` +
505
+ `an equivalent single quantifier matches the same strings`);
506
+ }
482
507
  }
483
508
  }
484
509
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bank20baht-validator",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Runtime validation for bahtui form/workflow JSON — the exact engine rules the client runs, on the server.",
6
6
  "license": "MIT",