diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8c88114..9fe73c3 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,4 +1,8 @@ ## 2026-04-27 - Path Traversal Vulnerability in File System Tools **Vulnerability:** The `FileTool` operations (`read`, `write`, `patch`) allowed arbitrary file system access because user-provided paths were used directly without sanitization, exposing a path traversal risk. **Learning:** File path inputs to SDK operations must be properly resolved and validated to prevent unauthorized access outside the intended working directory. Unrestricted access breaks the security boundaries of an agentic execution environment. -**Prevention:** Always use `path.resolve(process.cwd(), unsafePath)` and verify that the resulting absolute path begins with `process.cwd()`. Implement secure failure paths and never expose raw internal file structures on error. \ No newline at end of file +**Prevention:** Always use `path.resolve(process.cwd(), unsafePath)` and verify that the resulting absolute path begins with `process.cwd()`. Implement secure failure paths and never expose raw internal file structures on error. +## 2026-06-02 - Resource Exhaustion (DoS) Risk in External Operations +**Vulnerability:** External operations, specifically `ShellTool.run` (shell command execution) and `AgentConnector.chat` (network requests via Axios), lacked strict execution boundaries (timeouts and buffer limits). This allowed for hanging commands, excessive output generation, or stalled network requests to cause resource exhaustion or Denial of Service (DoS). +**Learning:** Agentic operations are susceptible to malicious or errant long-running processes or large outputs that can consume memory or block the event loop, causing the SDK to hang indefinitely. +**Prevention:** Always set strict limits on external invocations. Use `timeout` and `maxBuffer` limits for child processes (`execAsync`), and `timeout` on external API connections (e.g., Axios). diff --git a/dist/src/agents/connector.d.ts b/dist/src/agents/connector.d.ts index e171759..3791989 100644 --- a/dist/src/agents/connector.d.ts +++ b/dist/src/agents/connector.d.ts @@ -7,6 +7,7 @@ export interface ToolDefinition { export declare class AgentConnector { private core; private config; + private client; constructor(core: CoreEngine, config?: { provider?: string; apiKey?: string; diff --git a/dist/src/agents/connector.js b/dist/src/agents/connector.js index d1c7aed..1d8a98f 100644 --- a/dist/src/agents/connector.js +++ b/dist/src/agents/connector.js @@ -1,14 +1,54 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentConnector = void 0; const axios_1 = __importDefault(require("axios")); +const https = __importStar(require("https")); class AgentConnector { constructor(core, config = {}) { this.core = core; this.config = config; + // Optimize repeated API calls by enabling keep-alive for HTTP/HTTPS connections. + // This avoids expensive TCP/TLS handshakes (~100-200ms per request) on consecutive LLM calls. + this.client = axios_1.default.create({ + httpsAgent: new https.Agent({ keepAlive: true }), + timeout: 30000 // Prevent DoS: Enforce network timeout + }); } /** * Standard protocol to expose oneshotsx tools to any LLM @@ -39,7 +79,7 @@ class AgentConnector { return "Agent running in simulation mode. No API key provided."; } try { - const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', { + const response = await this.client.post('https://openrouter.ai/api/v1/chat/completions', { model: 'meta-llama/llama-3.1-70b-instruct', messages: [{ role: 'user', content: message }] }, { diff --git a/dist/src/tools/index.d.ts b/dist/src/tools/index.d.ts index f6fbbb5..0ede48c 100644 --- a/dist/src/tools/index.d.ts +++ b/dist/src/tools/index.d.ts @@ -10,9 +10,10 @@ export declare class ShellTool { export declare class FileTool { private core; constructor(core: CoreEngine); - read(path: string): Promise>; - write(path: string, content: string): Promise>; - patch(path: string, search: string, replace: string): Promise>; + private resolveAndValidatePath; + read(unsafePath: string): Promise>; + write(unsafePath: string, content: string): Promise>; + patch(unsafePath: string, search: string, replace: string): Promise>; } export declare class SearchTool { private core; diff --git a/dist/src/tools/index.js b/dist/src/tools/index.js index e8ea37b..13fe18b 100644 --- a/dist/src/tools/index.js +++ b/dist/src/tools/index.js @@ -37,6 +37,7 @@ exports.SearchTool = exports.FileTool = exports.ShellTool = void 0; const child_process_1 = require("child_process"); const util_1 = require("util"); const fs = __importStar(require("fs/promises")); +const path = __importStar(require("path")); const execAsync = (0, util_1.promisify)(child_process_1.exec); class ShellTool { constructor(core) { @@ -45,7 +46,11 @@ class ShellTool { async run(command) { this.core.log(`Executing shell command: ${command}`); return this.core.execute(async () => { - const { stdout, stderr } = await execAsync(command); + // Prevent DoS: Enforce execution timeouts and buffer limits + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, + maxBuffer: 1024 * 1024 * 10 // 10MB limit + }); return { stdout, stderr }; }); } @@ -55,23 +60,37 @@ class FileTool { constructor(core) { this.core = core; } - async read(path) { - this.core.log(`Reading file: ${path}`); - return this.core.execute(() => fs.readFile(path, 'utf-8')); + resolveAndValidatePath(unsafePath) { + const resolvedPath = path.resolve(process.cwd(), unsafePath); + if (!resolvedPath.startsWith(process.cwd() + path.sep) && resolvedPath !== process.cwd()) { + throw new Error('Access denied: Invalid path'); + } + return resolvedPath; } - async write(path, content) { - this.core.log(`Writing file: ${path}`); - return this.core.execute(() => fs.writeFile(path, content)); + async read(unsafePath) { + this.core.log(`Reading file: ${unsafePath}`); + return this.core.execute(() => { + const safePath = this.resolveAndValidatePath(unsafePath); + return fs.readFile(safePath, 'utf-8'); + }); + } + async write(unsafePath, content) { + this.core.log(`Writing file: ${unsafePath}`); + return this.core.execute(() => { + const safePath = this.resolveAndValidatePath(unsafePath); + return fs.writeFile(safePath, content); + }); } - async patch(path, search, replace) { - this.core.log(`Patching file: ${path}`); + async patch(unsafePath, search, replace) { + this.core.log(`Patching file: ${unsafePath}`); return this.core.execute(async () => { - const content = await fs.readFile(path, 'utf-8'); + const safePath = this.resolveAndValidatePath(unsafePath); + const content = await fs.readFile(safePath, 'utf-8'); const newContent = content.replace(search, replace); if (content === newContent) { - throw new Error(`Search string not found in ${path}`); + throw new Error(`Search string not found in file`); } - await fs.writeFile(path, newContent); + await fs.writeFile(safePath, newContent); }); } } diff --git a/src/agents/connector.ts b/src/agents/connector.ts index 82816e3..8c831ae 100644 --- a/src/agents/connector.ts +++ b/src/agents/connector.ts @@ -15,7 +15,8 @@ export class AgentConnector { // Optimize repeated API calls by enabling keep-alive for HTTP/HTTPS connections. // This avoids expensive TCP/TLS handshakes (~100-200ms per request) on consecutive LLM calls. this.client = axios.create({ - httpsAgent: new https.Agent({ keepAlive: true }) + httpsAgent: new https.Agent({ keepAlive: true }), + timeout: 30000 // Prevent DoS: Enforce network timeout }); } diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af329e..ca04048 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -12,7 +12,11 @@ export class ShellTool { async run(command: string): Promise> { this.core.log(`Executing shell command: ${command}`); return this.core.execute(async () => { - const { stdout, stderr } = await execAsync(command); + // Prevent DoS: Enforce execution timeouts and buffer limits + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, + maxBuffer: 1024 * 1024 * 10 // 10MB limit + }); return { stdout, stderr }; }); }