@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 +66 -0
- package/README.md +36 -7
- package/lib/ast.d.ts +41 -0
- package/lib/ast.js +68 -5
- package/package.json +3 -3
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
|
|
76
|
+
// Parse with custom security limits
|
|
77
77
|
const ast = parse(dotString, {
|
|
78
|
-
startRule: 'Dot',
|
|
79
|
-
maxHtmlNestingDepth: 200
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
41
|
+
"vite": "^7.0.8",
|
|
42
42
|
"vite-plugin-dts": "^4.5.3"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|