a2ui-project / a2ui-project/a2ui
[BUG]: Expression parser's recursion depth guard is unreachable (Dart and web_core)
- Ngôn ngữ chính
- TypeScript
- Star
- 16.4k
- Fork
- 1.3k
- Merge trung bình
- 2 ngày 13 giờ
- Pull request đã merge (30 ngày)
- 134
Mô tả
- [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.
Hướng dẫn đóng góp
Hướng nghiên cứu
The bug is in the expression parsers in dart/a2ui_core/lib/src/processing/expressions.dart and renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts. Start by reading the parseExpressionInternal method in both files to understand the recursion paths. The fix is to move the depth check into parseExpressionInternal and ensure depth is incremented when parsing function-call arguments. Verify the fix by running the provided reproduction code and ensuring an A2uiExpressionError is thrown for nesting >10.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- dart, typescript
- Lĩnh vực
- compilers
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 65/100