diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8c88114..2ec7339 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-03 - Resource Exhaustion (DoS) in Shell and Network Tools +**Vulnerability:** External operations like shell commands (`execAsync`) and network requests (`axios.post`) did not have enforced timeouts or buffer limits, creating a Denial of Service (DoS) vulnerability. Malicious input or hung processes could consume infinite memory or block execution indefinitely. +**Learning:** Whenever wrapping unconstrained external calls (e.g., shell, network), explicit limits must be set to enforce strict boundaries. +**Prevention:** Always supply `timeout` and `maxBuffer` constraints to `exec`/`execAsync` and `timeout` constraints to network requests (e.g. `axios`, `fetch`) to fail securely on resource exhaustion. 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..582769d 100644 --- a/dist/src/agents/connector.js +++ b/dist/src/agents/connector.js @@ -1,14 +1,53 @@ "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 }) + }); } /** * Standard protocol to expose oneshotsx tools to any LLM @@ -39,11 +78,12 @@ 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 }] }, { - headers: { 'Authorization': `Bearer ${this.config.apiKey}` } + headers: { 'Authorization': `Bearer ${this.config.apiKey}` }, + timeout: 30000 // Prevent network connections from hanging indefinitely }); return response.data.choices[0].message.content; } 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..79b000e 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 and memory exhaustion with timeouts and buffer limits + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, + maxBuffer: 10 * 1024 * 1024 + }); 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..18fa49d 100644 --- a/src/agents/connector.ts +++ b/src/agents/connector.ts @@ -54,7 +54,8 @@ export class AgentConnector { model: 'meta-llama/llama-3.1-70b-instruct', messages: [{ role: 'user', content: message }] }, { - headers: { 'Authorization': `Bearer ${this.config.apiKey}` } + headers: { 'Authorization': `Bearer ${this.config.apiKey}` }, + timeout: 30000 // Prevent network connections from hanging indefinitely }); return response.data.choices[0].message.content; } catch (err: any) { diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af329e..831c79e 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 and memory exhaustion with timeouts and buffer limits + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, + maxBuffer: 10 * 1024 * 1024 + }); return { stdout, stderr }; }); }