security(coding-agent): sanitized artifact names and wrapped extension and MCP tools

- Sanitized artifact filenames by normalizing tool names before composing spill paths.
- Applied `wrapToolWithMetaNotice` to custom tool adapters and RPC-host tools in agent-session setup.
- Wrapped SDK-registered extension/custom tools with the same meta-notice adapter during session creation.
This commit is contained in:
can1357
2026-06-17 01:53:24 +02:00
parent abe4453234
commit eaba315cbd
6 changed files with 61 additions and 9 deletions
@@ -250,7 +250,7 @@ import {
} from "../tool-discovery/tool-index";
import { assertEditableFile } from "../tools/auto-generated-guard";
import type { CheckpointState } from "../tools/checkpoint";
import { outputMeta } from "../tools/output-meta";
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
import { isAutoQaEnabled } from "../tools/report-tool-issue";
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo";
@@ -4611,7 +4611,7 @@ export class AgentSession {
});
for (const customTool of mcpTools) {
const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
const finalTool = (
this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
) as AgentTool;
@@ -4671,8 +4671,9 @@ export class AgentSession {
this.#rpcHostToolNames.clear();
for (const tool of rpcTools) {
const metaWrapped = wrapToolWithMetaNotice(tool);
const finalTool = (
this.#extensionRunner ? new ExtensionToolWrapper(tool, this.#extensionRunner) : tool
this.#extensionRunner ? new ExtensionToolWrapper(metaWrapped, this.#extensionRunner) : metaWrapped
) as AgentTool;
this.#toolRegistry.set(finalTool.name, finalTool);
this.#rpcHostToolNames.add(finalTool.name);
+19 -1
View File
@@ -7,6 +7,24 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
/**
* Sanitize a tool name for safe use as the middle segment of the artifact
* filename (`${id}.${toolType}.log`). Built-in tool names are fixed, but MCP,
* extension, and RPC-host tool names are arbitrary and may contain path
* separators (`/`, `\`) or traversal sequences (`..`) that would otherwise let
* a spilled artifact escape the artifacts directory. Collapse everything
* outside `[A-Za-z0-9_-]` to `_`, and cap the length so an arbitrarily long
* name cannot overflow the filesystem's filename limit (ENAMETOOLONG). Fall
* back to `tool` when nothing survives.
*/
function sanitizeToolType(toolType: string): string {
const sanitized = toolType
.replace(/[^A-Za-z0-9_-]+/g, "_")
.slice(0, 64)
.replace(/^_+|_+$/g, "");
return sanitized.length > 0 ? sanitized : "tool";
}
/**
* Manages artifact storage for a session.
*
@@ -83,7 +101,7 @@ export class ArtifactManager {
async allocatePath(toolType: string): Promise<{ id: string; path: string }> {
await this.#ensureDir();
const id = String(this.allocateId());
const filename = `${id}.${toolType}.log`;
const filename = `${id}.${sanitizeToolType(toolType)}.log`;
return { id, path: path.join(this.#dir, filename) };
}