@ts-graphviz/ast 3.0.4 → 3.0.5-next-dc3ef34316f5642c416711cb6a50704dbef7bb64

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,71 @@
1
1
  # @ts-graphviz/ast
2
2
 
3
+ ## 3.0.5-next-dc3ef34316f5642c416711cb6a50704dbef7bb64
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1533](https://github.com/ts-graphviz/ts-graphviz/pull/1533) [`ed770be`](https://github.com/ts-graphviz/ts-graphviz/commit/ed770be7fffc93b9171198c9a84270df7477185d) Thanks [@kamiazya](https://github.com/kamiazya)! - Add memory exhaustion protection with input size and AST node count limits
8
+
9
+ Addresses security vulnerability where extremely large inputs or inputs with excessive elements could cause memory exhaustion, leading to application crashes and potential DoS attacks.
10
+
11
+ ## Security Enhancements
12
+
13
+ ### Input Size Limit
14
+
15
+ - Added `maxInputSize` option to `parse()` function (default: 10MB)
16
+ - Validates input size before parsing to prevent memory exhaustion from extremely large DOT files
17
+ - Configurable limit allows flexibility for legitimate large graphs
18
+ - Can be disabled by setting to 0 (not recommended for untrusted inputs)
19
+
20
+ ### AST Node Count Limit
21
+
22
+ - Added `maxASTNodes` option to `parse()` function (default: 100,000 nodes)
23
+ - Tracks and limits the number of AST nodes created during parsing
24
+ - Prevents memory exhaustion from inputs with excessive elements
25
+ - Configurable limit for complex graphs when needed
26
+ - Can be disabled by setting to 0 (not recommended for untrusted inputs)
27
+
28
+ ## Changes
29
+
30
+ ### API Updates
31
+
32
+ - `CommonParseOptions` interface extended with `maxInputSize` and `maxASTNodes` options
33
+ - `Builder` class enhanced with node counting and validation
34
+ - `parse()` function validates input size before parsing
35
+ - Parser grammar updated to pass limits to Builder
36
+
37
+ ### Error Handling
38
+
39
+ - Input size violations throw `DotSyntaxError` with descriptive messages
40
+ - AST node count violations throw `DotSyntaxError` with actionable guidance
41
+ - Error messages include current values and suggestions for resolution
42
+
43
+ ### Testing
44
+
45
+ - Comprehensive test coverage for both limits
46
+ - Tests for normal usage, boundary conditions, and limit violations
47
+ - Tests for custom limit values and disabling limits
48
+ - All 62 parser tests passing
49
+
50
+ ### Documentation
51
+
52
+ - Updated `SECURITY.md` with detailed security protection information
53
+ - Added usage examples and best practices
54
+ - Documented recommendations for untrusted input handling
55
+
56
+ ## Security Impact
57
+
58
+ - Prevents DoS attacks via extremely large DOT files (hundreds of MB)
59
+ - Prevents memory exhaustion from inputs with tens of thousands of elements
60
+ - Default limits protect normal use cases while allowing customization
61
+ - Complements existing protections (HTML nesting depth, edge chain depth)
62
+ - Provides defense-in-depth security strategy
63
+
64
+ - [#1532](https://github.com/ts-graphviz/ts-graphviz/pull/1532) [`dc3ef34`](https://github.com/ts-graphviz/ts-graphviz/commit/dc3ef34316f5642c416711cb6a50704dbef7bb64) Thanks [@dependabot](https://github.com/apps/dependabot)! - build(deps-dev): bump vite from 7.0.2 to 7.0.8 in the npm_and_yarn group across 1 directory
65
+
66
+ - Updated dependencies [[`dc3ef34`](https://github.com/ts-graphviz/ts-graphviz/commit/dc3ef34316f5642c416711cb6a50704dbef7bb64)]:
67
+ - @ts-graphviz/common@3.0.4-next-dc3ef34316f5642c416711cb6a50704dbef7bb64
68
+
3
69
  ## 3.0.4
4
70
 
5
71
  ### Patch Changes
package/README.md CHANGED
@@ -73,18 +73,47 @@ The `parse` function accepts an optional second argument for configuration:
73
73
  ```ts
74
74
  import { parse } from "@ts-graphviz/ast";
75
75
 
76
- // Parse with custom HTML nesting depth limit
76
+ // Parse with custom security limits
77
77
  const ast = parse(dotString, {
78
- startRule: 'Dot', // Specify the starting rule (default: 'Dot')
79
- maxHtmlNestingDepth: 200 // Set maximum HTML nesting depth (default: 100)
78
+ startRule: 'Dot', // Specify the starting rule (default: 'Dot')
79
+ maxHtmlNestingDepth: 200, // Maximum HTML nesting depth (default: 100)
80
+ maxEdgeChainDepth: 2000, // Maximum edge chain depth (default: 1000)
81
+ maxInputSize: 20971520, // Maximum input size in bytes (default: 10MB)
82
+ maxASTNodes: 200000 // Maximum AST nodes (default: 100,000)
80
83
  });
81
84
  ```
82
85
 
86
+ **Available Options**:
87
+
88
+ | Option | Default | Description |
89
+ |--------|---------|-------------|
90
+ | `startRule` | `'Dot'` | Starting grammar rule for parsing |
91
+ | `maxHtmlNestingDepth` | `100` | Maximum depth of nested HTML-like structures |
92
+ | `maxEdgeChainDepth` | `1000` | Maximum depth of chained edges (e.g., `a -> b -> c -> ...`) |
93
+ | `maxInputSize` | `10485760` (10MB) | Maximum input size in bytes |
94
+ | `maxASTNodes` | `100000` | Maximum number of AST nodes to create |
95
+
83
96
  **Security Note**:
84
- - The `maxHtmlNestingDepth` option limits the depth of nested HTML-like structures in DOT files to prevent stack overflow attacks
85
- - The default limit of 100 is sufficient for normal use cases (typically <10 levels)
86
- - HTML-like labels are GraphViz DOT syntax, not browser HTML
87
- - For processing untrusted DOT files, see the validation guide in `@ts-graphviz/adapter` documentation
97
+
98
+ These limits protect against denial-of-service attacks:
99
+
100
+ - **`maxHtmlNestingDepth`**: Prevents stack overflow from deeply nested HTML-like structures
101
+ - Normal use cases: typically <10 levels
102
+ - HTML-like labels are GraphViz DOT syntax, not browser HTML
103
+
104
+ - **`maxEdgeChainDepth`**: Prevents stack overflow from deeply chained edges
105
+ - Example dangerous input: `a -> b -> c -> ... -> z (1000+ nodes)`
106
+
107
+ - **`maxInputSize`**: Prevents memory exhaustion from extremely large files
108
+ - Default 10MB is sufficient for most legitimate graphs
109
+ - Can be increased for known large graphs or disabled with `0` (not recommended for untrusted input)
110
+
111
+ - **`maxASTNodes`**: Prevents memory exhaustion from inputs with excessive elements
112
+ - Each DOT element creates multiple AST nodes
113
+ - Example: A single node statement (`node1;`) creates ~2-3 AST nodes
114
+ - Can be disabled with `0` (not recommended for untrusted input)
115
+
116
+ **Important**: When processing untrusted DOT files (e.g., user uploads), keep these limits enabled with conservative values appropriate for your environment. For additional validation of untrusted content, see the validation guide in [@ts-graphviz/adapter documentation](../adapter/README.md#security-considerations).
88
117
 
89
118
  ### Generating DOT Language
90
119
 
package/lib/ast.d.ts CHANGED
@@ -66,6 +66,23 @@ export declare interface ASTCommonPropaties {
66
66
  */
67
67
  export declare type ASTNode = LiteralASTNode | DotASTNode | GraphASTNode | AttributeASTNode | CommentASTNode | AttributeListASTNode | NodeRefASTNode | NodeRefGroupASTNode | EdgeASTNode | NodeASTNode | SubgraphASTNode;
68
68
 
69
+ /**
70
+ * Error thrown when the AST node count exceeds the maximum allowed limit.
71
+ * This error is thrown during parsing to prevent memory exhaustion attacks.
72
+ *
73
+ * @group Create AST
74
+ */
75
+ export declare class ASTNodeCountExceededError extends Error {
76
+ nodeCount: number;
77
+ maxNodes: number;
78
+ /**
79
+ * Constructor
80
+ * @param nodeCount - The current node count when the limit was exceeded
81
+ * @param maxNodes - The maximum allowed node count
82
+ */
83
+ constructor(nodeCount: number, maxNodes: number);
84
+ }
85
+
69
86
  /**
70
87
  * ASTToModel is a type that determines a model type from an AST.
71
88
  *
@@ -115,6 +132,8 @@ export declare interface AttributeListASTPropaties extends ASTCommonPropaties {
115
132
  */
116
133
  export declare class Builder implements ASTBuilder {
117
134
  private options?;
135
+ private nodeCount;
136
+ private maxNodes;
118
137
  /* Excluded from this release type: getLocation */
119
138
  /**
120
139
  * Constructor of Builder
@@ -128,8 +147,10 @@ export declare class Builder implements ASTBuilder {
128
147
  * @param props - Properties of the {@link ASTNode}
129
148
  * @param children - Children of the {@link ASTNode}
130
149
  * @returns An {@link ASTNode}
150
+ * @throws {ASTNodeCountExceededError} if the maximum number of AST nodes is exceeded
131
151
  */
132
152
  createElement<T extends ASTNode>(type: T['type'], props: any, children?: ASTChildNode<T>[]): T;
153
+ /* Excluded from this release type: resetNodeCount */
133
154
  }
134
155
 
135
156
  /**
@@ -143,6 +164,12 @@ export declare interface BuilderOptions {
143
164
  * It is used to specify the location of the builder.
144
165
  */
145
166
  locationFunction: () => FileRange;
167
+ /**
168
+ * Maximum allowed number of AST nodes to create.
169
+ * Default is 100000. Set to 0 to disable this limit.
170
+ * @default 100000
171
+ */
172
+ maxASTNodes?: number;
146
173
  }
147
174
 
148
175
  /**
@@ -201,6 +228,20 @@ export declare interface CommonParseOptions {
201
228
  * @default 1000
202
229
  */
203
230
  maxEdgeChainDepth?: number;
231
+ /**
232
+ * maxInputSize (optional): Maximum allowed input size in bytes.
233
+ * Default is 10MB (10485760 bytes). This limit prevents memory exhaustion from extremely large inputs.
234
+ * Set to 0 to disable this limit (not recommended for untrusted inputs).
235
+ * @default 10485760
236
+ */
237
+ maxInputSize?: number;
238
+ /**
239
+ * maxASTNodes (optional): Maximum allowed number of AST nodes to create during parsing.
240
+ * Default is 100000. This limit prevents memory exhaustion from inputs with excessive elements.
241
+ * Set to 0 to disable this limit (not recommended for untrusted inputs).
242
+ * @default 100000
243
+ */
244
+ maxASTNodes?: number;
204
245
  }
205
246
 
206
247
  /**
package/lib/ast.js CHANGED
@@ -1,4 +1,20 @@
1
1
  import { isNodeModel, isForwardRefNode, createModelsContext } from "@ts-graphviz/common";
2
+ class ASTNodeCountExceededError extends Error {
3
+ /**
4
+ * Constructor
5
+ * @param nodeCount - The current node count when the limit was exceeded
6
+ * @param maxNodes - The maximum allowed node count
7
+ */
8
+ constructor(nodeCount, maxNodes) {
9
+ super(
10
+ `AST node count (${nodeCount}) exceeds maximum allowed (${maxNodes}). Consider increasing 'maxASTNodes' option or simplifying the input.`
11
+ );
12
+ this.nodeCount = nodeCount;
13
+ this.maxNodes = maxNodes;
14
+ this.name = "ASTNodeCountExceededError";
15
+ }
16
+ }
17
+ const DEFAULT_MAX_AST_NODES = 1e5;
2
18
  class Builder {
3
19
  /**
4
20
  * Constructor of Builder
@@ -6,7 +22,10 @@ class Builder {
6
22
  */
7
23
  constructor(options) {
8
24
  this.options = options;
25
+ this.maxNodes = options?.maxASTNodes ?? DEFAULT_MAX_AST_NODES;
9
26
  }
27
+ nodeCount = 0;
28
+ maxNodes;
10
29
  /**
11
30
  * Get the current file range or null
12
31
  * @internal
@@ -21,8 +40,13 @@ class Builder {
21
40
  * @param props - Properties of the {@link ASTNode}
22
41
  * @param children - Children of the {@link ASTNode}
23
42
  * @returns An {@link ASTNode}
43
+ * @throws {ASTNodeCountExceededError} if the maximum number of AST nodes is exceeded
24
44
  */
25
45
  createElement(type, props, children = []) {
46
+ this.nodeCount++;
47
+ if (this.maxNodes > 0 && this.nodeCount > this.maxNodes) {
48
+ throw new ASTNodeCountExceededError(this.nodeCount, this.maxNodes);
49
+ }
26
50
  return {
27
51
  location: this.getLocation(),
28
52
  ...props,
@@ -30,6 +54,13 @@ class Builder {
30
54
  children
31
55
  };
32
56
  }
57
+ /**
58
+ * Reset the node count. Used internally by the parser between parse invocations.
59
+ * @internal
60
+ */
61
+ resetNodeCount() {
62
+ this.nodeCount = 0;
63
+ }
33
64
  }
34
65
  const createElement = Builder.prototype.createElement.bind(new Builder());
35
66
  class peg$SyntaxError extends SyntaxError {
@@ -2718,8 +2749,16 @@ function peg$parse(input, options) {
2718
2749
  let htmlNestingDepth = 0;
2719
2750
  let edgeChainDepth = 0;
2720
2751
  const b = new Builder({
2721
- locationFunction: location
2752
+ locationFunction: location,
2753
+ maxASTNodes: options.maxASTNodes
2722
2754
  });
2755
+ function resetState() {
2756
+ b.resetNodeCount();
2757
+ htmlNestingDepth = 0;
2758
+ edgeChainDepth = 0;
2759
+ edgeops.length = 0;
2760
+ }
2761
+ resetState();
2723
2762
  peg$result = peg$startRuleFunction();
2724
2763
  const peg$success = peg$result !== peg$FAILED && peg$currPos === input.length;
2725
2764
  function peg$throw() {
@@ -2752,14 +2791,32 @@ function peg$parse(input, options) {
2752
2791
  peg$throw();
2753
2792
  }
2754
2793
  }
2794
+ const DEFAULT_MAX_INPUT_SIZE = 10 * 1024 * 1024;
2755
2795
  function parse(input, options) {
2756
- const { startRule, filename, maxHtmlNestingDepth, maxEdgeChainDepth } = options ?? {};
2796
+ const {
2797
+ startRule,
2798
+ filename,
2799
+ maxHtmlNestingDepth,
2800
+ maxEdgeChainDepth,
2801
+ maxInputSize,
2802
+ maxASTNodes
2803
+ } = options ?? {};
2804
+ const inputSizeLimit = maxInputSize ?? DEFAULT_MAX_INPUT_SIZE;
2805
+ if (inputSizeLimit > 0) {
2806
+ const inputBytes = new TextEncoder().encode(input).length;
2807
+ if (inputBytes > inputSizeLimit) {
2808
+ throw new DotSyntaxError(
2809
+ `Input size (${inputBytes} bytes) exceeds maximum allowed size (${inputSizeLimit} bytes). Consider increasing 'maxInputSize' option or reducing the input size.`
2810
+ );
2811
+ }
2812
+ }
2757
2813
  try {
2758
2814
  return peg$parse(input, {
2759
2815
  startRule,
2760
2816
  filename,
2761
2817
  maxHtmlNestingDepth,
2762
- maxEdgeChainDepth
2818
+ maxEdgeChainDepth,
2819
+ maxASTNodes
2763
2820
  });
2764
2821
  } catch (e) {
2765
2822
  if (e instanceof peg$SyntaxError) {
@@ -2767,6 +2824,11 @@ function parse(input, options) {
2767
2824
  cause: e
2768
2825
  });
2769
2826
  }
2827
+ if (e instanceof ASTNodeCountExceededError) {
2828
+ throw new DotSyntaxError(e.message, {
2829
+ cause: e
2830
+ });
2831
+ }
2770
2832
  throw new Error("Unexpected parse error", {
2771
2833
  cause: e
2772
2834
  });
@@ -3108,7 +3170,7 @@ function convertComment(value, kind) {
3108
3170
  }
3109
3171
  function convertClusterChildren(context, model) {
3110
3172
  return Array.from(
3111
- function* () {
3173
+ (function* () {
3112
3174
  for (const [key, value] of model.values) {
3113
3175
  yield convertAttribute(key, value);
3114
3176
  }
@@ -3138,7 +3200,7 @@ function convertClusterChildren(context, model) {
3138
3200
  }
3139
3201
  yield context.convert(edge);
3140
3202
  }
3141
- }()
3203
+ })()
3142
3204
  );
3143
3205
  }
3144
3206
  const AttributeListPlugin = {
@@ -3605,6 +3667,7 @@ function toModel(ast, options) {
3605
3667
  return new ToModelConverter(options).convert(ast);
3606
3668
  }
3607
3669
  export {
3670
+ ASTNodeCountExceededError,
3608
3671
  Builder,
3609
3672
  DotSyntaxError,
3610
3673
  FromModelConverter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-graphviz/ast",
3
- "version": "3.0.4",
3
+ "version": "3.0.5-next-dc3ef34316f5642c416711cb6a50704dbef7bb64",
4
4
  "description": "Graphviz AST(Abstract Syntax Tree) Utilities",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/ts-graphviz/ts-graphviz#readme",
@@ -33,12 +33,12 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@ts-graphviz/common": "^3.0.3"
36
+ "@ts-graphviz/common": "^3.0.4-next-dc3ef34316f5642c416711cb6a50704dbef7bb64"
37
37
  },
38
38
  "devDependencies": {
39
39
  "peggy": "^5.0.6",
40
40
  "typescript": "^5.8.2",
41
- "vite": "^7.0.2",
41
+ "vite": "^7.0.8",
42
42
  "vite-plugin-dts": "^4.5.3"
43
43
  },
44
44
  "engines": {