@ts-graphviz/ast 3.0.3 → 3.0.4-next-6966a6699e87e2c74e7348aa6fdc2c50ae11b308

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @ts-graphviz/ast
2
2
 
3
+ ## 3.0.4-next-6966a6699e87e2c74e7348aa6fdc2c50ae11b308
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1526](https://github.com/ts-graphviz/ts-graphviz/pull/1526) [`00aaf2f`](https://github.com/ts-graphviz/ts-graphviz/commit/00aaf2ff6ef6fa8b6611ec2a477bc46b76fdebaf) Thanks [@kamiazya](https://github.com/kamiazya)! - Fix critical stack overflow vulnerability in HTML string parser
8
+
9
+ This patch addresses a critical security vulnerability where deeply nested HTML-like structures in DOT files could cause stack overflow, leading to application crashes and potential DoS attacks.
10
+
11
+ **Changes:**
12
+
13
+ - Added depth limit (default: 100) to HTML string parsing in PEG grammar
14
+ - Introduced `maxHtmlNestingDepth` option to `parse()` function for custom depth limits
15
+ - Improved parser to track and limit HTML nesting depth during parsing
16
+
17
+ **Security Impact:**
18
+
19
+ - Prevents stack overflow attacks from maliciously crafted DOT files with deep HTML nesting
20
+ - Normal use cases (typically <10 levels) are unaffected
21
+ - Configurable limit allows complex parsing when needed
22
+
23
+ **Breaking Changes:**
24
+ None. This is a backward-compatible security fix with sensible defaults.
25
+
3
26
  ## 3.0.3
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -66,6 +66,22 @@ console.log(ast);
66
66
  // Output: A DotASTNode representing the DOT structure
67
67
  ```
68
68
 
69
+ #### Parser Options
70
+
71
+ The `parse` function accepts an optional second argument for configuration:
72
+
73
+ ```ts
74
+ import { parse } from "@ts-graphviz/ast";
75
+
76
+ // Parse with custom HTML nesting depth limit
77
+ const ast = parse(dotString, {
78
+ startRule: 'Dot', // Specify the starting rule (default: 'Dot')
79
+ maxHtmlNestingDepth: 200 // Set maximum HTML nesting depth (default: 100)
80
+ });
81
+ ```
82
+
83
+ **Security Note**: The `maxHtmlNestingDepth` option limits the depth of nested HTML-like structures in DOT files to prevent stack overflow attacks. The default limit of 100 is sufficient for normal use cases (typically <10 levels). Increase this value only if you have legitimate deeply nested HTML structures.
84
+
69
85
  ### Generating DOT Language
70
86
 
71
87
  ```ts
package/lib/ast.d.ts CHANGED
@@ -189,6 +189,12 @@ export declare interface CommonParseOptions {
189
189
  * filename (optional): A string value that is used to identify the file to be parsed.
190
190
  */
191
191
  filename?: string;
192
+ /**
193
+ * maxHtmlNestingDepth (optional): Maximum allowed nesting depth for HTML-like strings.
194
+ * Default is 100. This limit prevents stack overflow attacks from deeply nested HTML structures.
195
+ * @default 100
196
+ */
197
+ maxHtmlNestingDepth?: number;
192
198
  }
193
199
 
194
200
  /**
package/lib/ast.js CHANGED
@@ -485,16 +485,24 @@ function peg$parse(input, options) {
485
485
  []
486
486
  );
487
487
  }
488
- function peg$f44(v) {
489
- return "<" + v.join("") + ">";
488
+ function peg$f44() {
489
+ if (htmlNestingDepth >= MAX_HTML_NESTING_DEPTH) {
490
+ error(`HTML nesting depth exceeds maximum allowed depth of ${MAX_HTML_NESTING_DEPTH}`);
491
+ }
492
+ htmlNestingDepth++;
493
+ return true;
490
494
  }
491
495
  function peg$f45(v) {
492
- return v;
496
+ htmlNestingDepth--;
497
+ return "<" + v.join("") + ">";
493
498
  }
494
499
  function peg$f46(v) {
500
+ return v;
501
+ }
502
+ function peg$f47(v) {
495
503
  return v.join("");
496
504
  }
497
- function peg$f47(chars) {
505
+ function peg$f48(chars) {
498
506
  return b.createElement(
499
507
  "Literal",
500
508
  {
@@ -504,13 +512,13 @@ function peg$parse(input, options) {
504
512
  []
505
513
  );
506
514
  }
507
- function peg$f48() {
515
+ function peg$f49() {
508
516
  return text();
509
517
  }
510
- function peg$f49(v) {
518
+ function peg$f50(v) {
511
519
  return v[1] === '"' ? '"' : v[0] + v[1];
512
520
  }
513
- function peg$f50() {
521
+ function peg$f51() {
514
522
  return "";
515
523
  }
516
524
  let peg$currPos = options.peg$currPos | 0;
@@ -2257,7 +2265,7 @@ function peg$parse(input, options) {
2257
2265
  return s0;
2258
2266
  }
2259
2267
  function peg$parsehtml_raw_string() {
2260
- let s0, s1, s2, s3;
2268
+ let s0, s1, s2, s3, s4;
2261
2269
  s0 = peg$currPos;
2262
2270
  if (input.charCodeAt(peg$currPos) === 60) {
2263
2271
  s1 = peg$c30;
@@ -2269,30 +2277,42 @@ function peg$parse(input, options) {
2269
2277
  }
2270
2278
  }
2271
2279
  if (s1 !== peg$FAILED) {
2272
- s2 = [];
2273
- s3 = peg$parsehtml_char();
2274
- if (s3 === peg$FAILED) {
2275
- s3 = peg$parsehtml_raw_string();
2280
+ peg$savedPos = peg$currPos;
2281
+ s2 = peg$f44();
2282
+ if (s2) {
2283
+ s2 = void 0;
2284
+ } else {
2285
+ s2 = peg$FAILED;
2276
2286
  }
2277
- while (s3 !== peg$FAILED) {
2278
- s2.push(s3);
2279
- s3 = peg$parsehtml_char();
2280
- if (s3 === peg$FAILED) {
2281
- s3 = peg$parsehtml_raw_string();
2287
+ if (s2 !== peg$FAILED) {
2288
+ s3 = [];
2289
+ s4 = peg$parsehtml_char();
2290
+ if (s4 === peg$FAILED) {
2291
+ s4 = peg$parsehtml_raw_string();
2282
2292
  }
2283
- }
2284
- if (input.charCodeAt(peg$currPos) === 62) {
2285
- s3 = peg$c31;
2286
- peg$currPos++;
2287
- } else {
2288
- s3 = peg$FAILED;
2289
- if (peg$silentFails === 0) {
2290
- peg$fail(peg$e39);
2293
+ while (s4 !== peg$FAILED) {
2294
+ s3.push(s4);
2295
+ s4 = peg$parsehtml_char();
2296
+ if (s4 === peg$FAILED) {
2297
+ s4 = peg$parsehtml_raw_string();
2298
+ }
2299
+ }
2300
+ if (input.charCodeAt(peg$currPos) === 62) {
2301
+ s4 = peg$c31;
2302
+ peg$currPos++;
2303
+ } else {
2304
+ s4 = peg$FAILED;
2305
+ if (peg$silentFails === 0) {
2306
+ peg$fail(peg$e39);
2307
+ }
2308
+ }
2309
+ if (s4 !== peg$FAILED) {
2310
+ peg$savedPos = s0;
2311
+ s0 = peg$f45(s3);
2312
+ } else {
2313
+ peg$currPos = s0;
2314
+ s0 = peg$FAILED;
2291
2315
  }
2292
- }
2293
- if (s3 !== peg$FAILED) {
2294
- peg$savedPos = s0;
2295
- s0 = peg$f44(s2);
2296
2316
  } else {
2297
2317
  peg$currPos = s0;
2298
2318
  s0 = peg$FAILED;
@@ -2338,7 +2358,7 @@ function peg$parse(input, options) {
2338
2358
  }
2339
2359
  if (s4 !== peg$FAILED) {
2340
2360
  peg$savedPos = s2;
2341
- s2 = peg$f45(s4);
2361
+ s2 = peg$f46(s4);
2342
2362
  } else {
2343
2363
  peg$currPos = s2;
2344
2364
  s2 = peg$FAILED;
@@ -2381,7 +2401,7 @@ function peg$parse(input, options) {
2381
2401
  }
2382
2402
  if (s4 !== peg$FAILED) {
2383
2403
  peg$savedPos = s2;
2384
- s2 = peg$f45(s4);
2404
+ s2 = peg$f46(s4);
2385
2405
  } else {
2386
2406
  peg$currPos = s2;
2387
2407
  s2 = peg$FAILED;
@@ -2396,7 +2416,7 @@ function peg$parse(input, options) {
2396
2416
  }
2397
2417
  if (s1 !== peg$FAILED) {
2398
2418
  peg$savedPos = s0;
2399
- s1 = peg$f46(s1);
2419
+ s1 = peg$f47(s1);
2400
2420
  }
2401
2421
  s0 = s1;
2402
2422
  return s0;
@@ -2431,7 +2451,7 @@ function peg$parse(input, options) {
2431
2451
  }
2432
2452
  if (s3 !== peg$FAILED) {
2433
2453
  peg$savedPos = s0;
2434
- s0 = peg$f47(s2);
2454
+ s0 = peg$f48(s2);
2435
2455
  } else {
2436
2456
  peg$currPos = s0;
2437
2457
  s0 = peg$FAILED;
@@ -2469,7 +2489,7 @@ function peg$parse(input, options) {
2469
2489
  s2 = peg$parseSourceCharacter();
2470
2490
  if (s2 !== peg$FAILED) {
2471
2491
  peg$savedPos = s0;
2472
- s0 = peg$f48();
2492
+ s0 = peg$f49();
2473
2493
  } else {
2474
2494
  peg$currPos = s0;
2475
2495
  s0 = peg$FAILED;
@@ -2520,7 +2540,7 @@ function peg$parse(input, options) {
2520
2540
  }
2521
2541
  if (s1 !== peg$FAILED) {
2522
2542
  peg$savedPos = s0;
2523
- s1 = peg$f49(s1);
2543
+ s1 = peg$f50(s1);
2524
2544
  }
2525
2545
  s0 = s1;
2526
2546
  return s0;
@@ -2541,7 +2561,7 @@ function peg$parse(input, options) {
2541
2561
  s2 = peg$parseLineTerminatorSequence();
2542
2562
  if (s2 !== peg$FAILED) {
2543
2563
  peg$savedPos = s0;
2544
- s0 = peg$f50();
2564
+ s0 = peg$f51();
2545
2565
  } else {
2546
2566
  peg$currPos = s0;
2547
2567
  s0 = peg$FAILED;
@@ -2685,6 +2705,8 @@ function peg$parse(input, options) {
2685
2705
  return str;
2686
2706
  }
2687
2707
  const edgeops = [];
2708
+ const MAX_HTML_NESTING_DEPTH = options.maxHtmlNestingDepth ?? 100;
2709
+ let htmlNestingDepth = 0;
2688
2710
  const b = new Builder({
2689
2711
  locationFunction: location
2690
2712
  });
@@ -2721,9 +2743,9 @@ function peg$parse(input, options) {
2721
2743
  }
2722
2744
  }
2723
2745
  function parse(input, options) {
2724
- const { startRule, filename } = options ?? {};
2746
+ const { startRule, filename, maxHtmlNestingDepth } = options ?? {};
2725
2747
  try {
2726
- return peg$parse(input, { startRule, filename });
2748
+ return peg$parse(input, { startRule, filename, maxHtmlNestingDepth });
2727
2749
  } catch (e) {
2728
2750
  if (e instanceof peg$SyntaxError) {
2729
2751
  throw new DotSyntaxError(e.message, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-graphviz/ast",
3
- "version": "3.0.3",
3
+ "version": "3.0.4-next-6966a6699e87e2c74e7348aa6fdc2c50ae11b308",
4
4
  "description": "Graphviz AST(Abstract Syntax Tree) Utilities",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/ts-graphviz/ts-graphviz#readme",