a2ui-project / a2ui-project/a2ui

[BUG]: Expression parser's recursion depth guard is unreachable (Dart and web_core)

Open
#2,490 2 comments 0 reactions 1 assignee Claimed by @Varun-S10 View on GitHub
P2 status: first-line-handled status: needs review type: bug
Dominant language
TypeScript
Stars
16.4k
Forks
1.3k
Avg merge
2d 13h
Merged PRs (30d)
134

Description

- [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.

Contributor guide

Open the contributing guide

Research direction

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.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, typescript
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.