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:
@@ -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);
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user