From b5e4e32de1d6d6a516ab7101d07af269f11f8d4b Mon Sep 17 00:00:00 2001 From: thirdbase1 <196722958+thirdbase1@users.noreply.github.com> Date: Tue, 26 May 2026 21:16:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Fix=20unbounded=20execution=20DoS=20risks=20in=20agent=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds 30s timeout and 5MB buffer limit to `ShellTool` execution. - Adds 30s timeout to `AgentConnector` network requests. - Appends vulnerability learning to `.jules/sentinel.md` journal. --- .jules/sentinel.md | 6 ++++- dist/src/agents/connector.d.ts | 1 + dist/src/agents/connector.js | 43 +++++++++++++++++++++++++++++++++- dist/src/tools/index.d.ts | 7 +++--- dist/src/tools/index.js | 43 ++++++++++++++++++++++++---------- src/agents/connector.ts | 4 +++- src/tools/index.ts | 6 ++++- 7 files changed, 91 insertions(+), 19 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8c88114..e3f7bec 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-26 - Unbounded Agent Execution DoS +**Vulnerability:** Shell and network operations executed by the autonomous agent (`ShellTool` and `AgentConnector`) had no timeouts or buffer limits. +**Learning:** Agentic operations are highly susceptible to Denial of Service (DoS) and resource exhaustion if underlying OS commands or network requests hang indefinitely or return massive payloads. +**Prevention:** Always enforce strict boundaries (e.g., max execution time of 30s, max output buffers) on all external operations performed on behalf of an agent to prevent host system 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..9277a10 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. + this.client = axios_1.default.create({ + httpsAgent: new https.Agent({ keepAlive: true }), + // Security: Prevent resource exhaustion by enforcing network timeout + 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..54079d4 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 infinite loops or massive outputs + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, // 30 seconds max execution time + 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..2f13b9c 100644 --- a/src/agents/connector.ts +++ b/src/agents/connector.ts @@ -15,7 +15,9 @@ 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 }), + // Security: Prevent resource exhaustion by enforcing network timeout + timeout: 30000 }); } diff --git a/src/tools/index.ts b/src/tools/index.ts index 6af329e..860163a 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 infinite loops or massive outputs + const { stdout, stderr } = await execAsync(command, { + timeout: 30000, // 30 seconds max execution time + maxBuffer: 5 * 1024 * 1024 // 5MB max output buffer + }); return { stdout, stderr }; }); }