a2ui-project / a2ui-project/a2ui
[BUG]: Expression parser's recursion depth guard is unreachable (Dart and web_core)
- Vorherrschende Sprache
- TypeScript
- Sterne
- 16.4k
- Forks
- 1.3k
- Ø Merge
- 2 T. 13 Std.
- Gemergte PRs (30 T.)
- 134
Beschreibung
- [x] I have searched the existing issues to make sure this bug has not already been reported.
## Describe the Bug
`ExpressionParser` declares a maximum recursion depth of 10, but nothing enforces it. The check lives in `parse()`, which is only ever entered at depth 0, while both recursive paths bypass it:
- **Nested interpolations** (`${${...}}`) recurse through `parseExpression()`, which accepts a `depth` but never compares it against the limit.
- **Function-call arguments** (`f(a: g(b: ...))`) recurse through `parseExpressionInternal()` with `depth` passed through unchanged, so that nesting is never counted at all.
Recursion on parse is therefore unbounded. Both implementations share the shape and the defect:
| | limit declared | reachable | deeply nested template |
| --- | --- | --- | --- |
| `dart/a2ui_core` | [`maxDepth = 10`](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/dart/a2ui_core/lib/src/processing/expressions.dart#L20) | no | `StackOverflowError` |
| `renderers/web_core` | [`MAX_DEPTH = 10`](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts#L29) | no | `RangeError: Maximum call stack size exceeded` |
The relevant lines in Dart are [L25](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/dart/a2ui_core/lib/src/processing/expressions.dart#L25) (the only check), [L101](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/dart/a2ui_core/lib/src/processing/expressions.dart#L101) (`parseExpression`, no check) and [L180](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/dart/a2ui_core/lib/src/processing/expressions.dart#L180) (arguments, depth not incremented); in web_core they are [L36](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts#L36), [L115](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts#L115) and [L198](https://github.com/a2ui-project/a2ui/blob/676a8999936b17070195230d396c18d14a22d64d/renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts#L198).
Nesting depth is agent-controlled: `formatString` hands its `value` argument straight to the parser, and that argument is an ordinary string in the message payload.
## Steps to Reproduce
Dart, against `dart/a2ui_core` at main (676a899):
```dart
final parser = ExpressionParser();
String nestedCalls(int n) => '\${${'f(a: ' * n}1${')' * n}}';
parser.parse(nestedCalls(1000)); // parses fine; the guard never fires
parser.parse(nestedCalls(5000)); // StackOverflowError
```
web_core, against the published `@a2ui/web_core@0.10.7`:
```js
const p = new ExpressionParser();
const nested = (n) => '${'.repeat(n) + 'x' + '}'.repeat(n);
p.parse(nested(1000)); // parses fine
p.parse(nested(5000)); // RangeError: Maximum call stack size exceeded
```
Measured for both nesting forms (`${f(a: f(a: ...))}` and `${${...}}`) in both languages:
| nesting depth | Dart | web_core |
| --- | --- | --- |
| 10 | parsed, no error | parsed, no error |
| 100 | parsed, no error | parsed, no error |
| 1000 | parsed, no error | parsed, no error |
| 5000 | `StackOverflowError` | `RangeError` |
## Expected Behavior
A template nested past the declared limit is rejected with `A2uiExpressionError`, which is what the guard was written to throw and what the rest of the expression layer raises. Today the caller instead gets a `StackOverflowError` / `RangeError`, outside the `A2uiError` hierarchy consumers handle — and in Dart, `avoid_catching_errors` (enabled repo-wide) discourages catching it at all.
## Environment Details
- **OS**: macOS
- **Browser/Platform**: Dart 3.12.2, Node.js 25.2.1
- **SDK/Package Name & Version**: `a2ui_core` at main (676a899), `@a2ui/web_core` 0.10.7
- **Protocol Version**: v0.9
## Additional Context
The fix has the same shape in both languages: move the depth check into `parseExpressionInternal()` — the single point both recursive paths pass through — and pass `depth + 1` when parsing function-call arguments.
I have the Dart side ready with tests and will open a PR for it shortly.
I am deliberately not sending the web_core change yet: #2433 is currently editing that same file to add template length and parts caps. Those caps are orthogonal to this one — they bound how large an input is, not how deeply it nests, so neither stops the recursion described here — and the fix is best applied on top of that PR rather than conflicting with it. Happy to send it as a follow-up once it lands.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.