fix(security): encode URL path segments and bound SSE buffering

- endpoints: encodeURIComponent the id segments (task_id, app_id, node_id,
  schema_id) interpolated into request URLs. task_id in particular comes from
  the server's async-submit response and is fetched back with the bearer token
  attached, so an unencoded value could steer the authenticated follow-up
  request to a different path on the host.
- stream (SSE parser): cap the in-memory buffer (16 MiB). A stream that never
  emits a newline, or that builds one enormous event from many data: lines,
  could otherwise grow the buffer without bound and exhaust process memory.

https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1
This commit is contained in:
Claude
2026-05-29 12:34:22 +00:00
parent 3e4f1f0ebf
commit d24f203d68
2 changed files with 21 additions and 4 deletions
+4 -4
View File
@@ -24,13 +24,13 @@ export function videoGenerateEndpoint(baseUrl: string): string {
// ---- Async Task Query ----
export function taskEndpoint(baseUrl: string, taskId: string): string {
return `${baseUrl}/api/v1/tasks/${taskId}`;
return `${baseUrl}/api/v1/tasks/${encodeURIComponent(taskId)}`;
}
// ---- Application (Agent / Workflow) ----
export function appCompletionEndpoint(baseUrl: string, appId: string): string {
return `${baseUrl}/api/v1/apps/${appId}/completion`;
return `${baseUrl}/api/v1/apps/${encodeURIComponent(appId)}/completion`;
}
// ---- Memory (DashScope v2) ----
@@ -48,7 +48,7 @@ export function memoryListEndpoint(baseUrl: string): string {
}
export function memoryNodeEndpoint(baseUrl: string, nodeId: string): string {
return `${baseUrl}/api/v2/apps/memory/memory_nodes/${nodeId}`;
return `${baseUrl}/api/v2/apps/memory/memory_nodes/${encodeURIComponent(nodeId)}`;
}
// ---- Speech Synthesis (TTS) ----
@@ -70,7 +70,7 @@ export function profileSchemaEndpoint(baseUrl: string): string {
}
export function userProfileEndpoint(baseUrl: string, schemaId: string): string {
return `${baseUrl}/api/v2/apps/memory/profile_schemas/${schemaId}/profiles`;
return `${baseUrl}/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`;
}
// ---- MCP Services (Streamable HTTP) ----
+17
View File
@@ -1,3 +1,6 @@
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
export interface ServerSentEvent {
event?: string;
data: string;
@@ -11,12 +14,20 @@ export async function* parseSSE(response: Response): AsyncGenerator<ServerSentEv
const decoder = new TextDecoder();
let buffer = "";
// Guard against a hostile or malfunctioning stream that never emits a newline
// (or builds a single absurdly large event): bound the in-memory buffer so the
// parser cannot be driven to exhaust process memory.
const MAX_SSE_BUFFER = 16 * 1024 * 1024; // 16 MiB
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
if (buffer.length > MAX_SSE_BUFFER) {
throw new BailianError("SSE stream exceeded the maximum buffer size.", ExitCode.GENERAL);
}
const lines = buffer.split("\n");
buffer = lines.pop() || "";
@@ -43,6 +54,12 @@ export async function* parseSSE(response: Response): AsyncGenerator<ServerSentEv
switch (field) {
case "data":
event.data = event.data !== undefined ? `${event.data}\n${value}` : value;
if (event.data.length > MAX_SSE_BUFFER) {
throw new BailianError(
"SSE event exceeded the maximum buffer size.",
ExitCode.GENERAL,
);
}
break;
case "event":
event.event = value;