feat(gateway): tool path hardening + sandbox escape prevention (P8-016) (#177)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #177.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import { guardPath, SandboxEscapeError } from './path-guard.js';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const MAX_OUTPUT_BYTES = 100 * 1024; // 100 KB
|
||||
@@ -68,22 +68,6 @@ function extractBaseCommand(command: string): string {
|
||||
return firstToken.split('/').pop() ?? firstToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a user-supplied cwd to within the sandbox directory.
|
||||
* If the resolved path escapes the sandbox (via ../ or absolute path outside),
|
||||
* falls back to the sandbox directory itself.
|
||||
*/
|
||||
function clampCwd(sandboxDir: string, requestedCwd?: string): string {
|
||||
if (!requestedCwd) return sandboxDir;
|
||||
const resolved = resolve(sandboxDir, requestedCwd);
|
||||
const rel = relative(sandboxDir, resolved);
|
||||
if (rel.startsWith('..') || rel.startsWith('/')) {
|
||||
// Escape attempt — fall back to sandbox root
|
||||
return sandboxDir;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function runCommand(
|
||||
command: string,
|
||||
options: { timeoutMs: number; cwd?: string },
|
||||
@@ -185,7 +169,21 @@ export function createShellTools(sandboxDir?: string): ToolDefinition[] {
|
||||
}
|
||||
|
||||
const timeoutMs = Math.min(timeout ?? DEFAULT_TIMEOUT_MS, 60_000);
|
||||
const safeCwd = clampCwd(defaultCwd, cwd);
|
||||
let safeCwd: string;
|
||||
try {
|
||||
safeCwd = guardPath(cwd ?? '.', defaultCwd);
|
||||
} catch (err) {
|
||||
if (err instanceof SandboxEscapeError) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
|
||||
details: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
|
||||
details: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await runCommand(command, {
|
||||
timeoutMs,
|
||||
|
||||
Reference in New Issue
Block a user