a2ui-project / a2ui-project/a2ui

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

Abierto
#2,490 2 comentarios 0 reacciones 1 asignado Reclamado por @Varun-S10 Ver en GitHub
P2 status: first-line-handled status: needs review type: bug
Lenguaje dominante
TypeScript
Estrellas
16.4k
Forks
1.3k
Merge medio
2 d 13 h
PR fusionados (30 d)
134

Descripción

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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.