diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8c88114..0461bab 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-01 - DoS / Resource Exhaustion in Shell & Network Operations +**Vulnerability:** Shell tool executions (`execAsync`) lacked boundaries, allowing infinite loops and memory overconsumption. Additionally, external agent connector requests using `axios` lacked timeouts, allowing network requests to hang indefinitely. Both vectors could be exploited for Denial of Service (DoS) attacks. +**Learning:** All system boundaries (I/O, network, shell execution) must enforce strict operational constraints to prevent resource starvation in agentic operations. +**Prevention:** Hardcode sane defaults like a 30s timeout and memory buffer limits (`maxBuffer: 1024 * 1024`) on all shell executions, and apply timeouts directly to all Axios client instances. 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..dc9b488 100644 --- a/dist/src/agents/connector.js +++ b/dist/src/agents/connector.js @@ -1,14 +1,55 @@ "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. + // Security: Add timeout to prevent network request hanging (DoS protection) + this.client = axios_1.default.create({ + httpsAgent: new https.Agent({ keepAlive: true }), + timeout: 30000 + }); } /** * Standard protocol to expose oneshotsx tools to any LLM @@ -39,7 +80,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..d3eb654 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,8 @@ 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, maxBuffer: 1024 * 1024 }); return { stdout, stderr }; }); } @@ -55,23 +57,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..d4e5bd4 100644 --- a/src/agents/connector.ts +++ b/src/agents/connector.ts @@ -14,8 +14,10 @@ export class AgentConnector { constructor(private core: CoreEngine, private config: { provider?: string; apiKey?: string } = {}) { // 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. + // Security: Add timeout to prevent network request hanging (DoS protection) this.client = axios.create({ - httpsAgent: new https.Agent({ keepAlive: true }) + httpsAgent: new https.Agent({ keepAlive: true }), + timeout: 30000 }); } diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af329e..03e91cc 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -12,7 +12,8 @@ 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, maxBuffer: 1024 * 1024 }); return { stdout, stderr }; }); }