diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8c88114..62b5c84 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-05-23 - Resource Exhaustion/DoS Vulnerability in Agent Tools +**Vulnerability:** Shell execution tools and external API connectors lacked explicit resource constraints (timeouts, max buffer limits). This could allow malicious or runaway processes to consume excessive CPU, memory, or network connections, leading to a Denial of Service (DoS) of the agent's environment. +**Learning:** In autonomous environments, it's critical to assume external processes or network calls can hang indefinitely or produce enormous output. Failing to bound these operations creates a severe reliability and security risk. +**Prevention:** Enforce strict resource boundaries on all unbounded operations. Specifically, add `timeout` and `maxBuffer` limits to shell execution commands, and enforce explicit `timeout` limits on HTTP/HTTPS network requests. 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..8dc06ae 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 // Security: Prevent DoS by enforcing a maximum wait time of 30 seconds + }); } /** * 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..530d551 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); + // Security: Enforce strict boundaries to prevent DoS via resource exhaustion + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, // 30 seconds max execution + maxBuffer: 5 * 1024 * 1024 // 5MB max output buffer + }); 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..6d9e2e8 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 // Security: Prevent DoS by enforcing a maximum wait time of 30 seconds }); } diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af329e..bdbb5a8 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); + // Security: Enforce strict boundaries to prevent DoS via resource exhaustion + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, // 30 seconds max execution + maxBuffer: 5 * 1024 * 1024 // 5MB max output buffer + }); return { stdout, stderr }; }); }