-
Notifications
You must be signed in to change notification settings - Fork 20
Security/harden shell cicd #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bde56b8
769fbc0
9c47809
9dd2f4c
445c912
8b9ef58
91e4469
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { existsSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { mkdirSync, writeFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join, dirname, sep } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
@@ -31,9 +31,6 @@ export { isSafe, touchesMemory, rewritePaths }; | |
| const log = (msg: string) => _log("pre", msg); | ||
|
|
||
| const __bundleDir = dirname(fileURLToPath(import.meta.url)); | ||
| const SHELL_BUNDLE = existsSync(join(__bundleDir, "shell", "deeplake-shell.js")) | ||
| ? join(__bundleDir, "shell", "deeplake-shell.js") | ||
| : join(__bundleDir, "..", "shell", "deeplake-shell.js"); | ||
|
|
||
| export interface PreToolUseInput { | ||
| session_id: string; | ||
|
|
@@ -131,7 +128,11 @@ export function getShellCommand(toolName: string, toolInput: Record<string, unkn | |
| const flags: string[] = ["-r"]; | ||
| if (toolInput["-i"]) flags.push("-i"); | ||
| if (toolInput["-n"]) flags.push("-n"); | ||
| return `grep ${flags.join(" ")} '${pattern}' /`; | ||
| // Single-quote the pattern safely: escape any embedded single quotes | ||
| // so the string can never break out of the shell quoting context if | ||
| // this command string is ever forwarded to a shell executor. | ||
| const escaped = pattern.replace(/'/g, "'\\''"); | ||
| return `grep ${flags.join(" ")} '${escaped}' /`; | ||
| } | ||
| break; | ||
| } | ||
|
|
@@ -189,13 +190,6 @@ export function extractGrepParams( | |
| return null; | ||
| } | ||
|
|
||
| function buildFallbackDecision(shellCmd: string, shellBundle = SHELL_BUNDLE): ClaudePreToolDecision { | ||
| return buildAllowDecision( | ||
| `node "${shellBundle}" -c "${shellCmd.replace(/"/g, '\\"')}"`, | ||
| `[DeepLake shell] ${shellCmd}`, | ||
| ); | ||
| } | ||
|
|
||
| interface ClaudePreToolDeps { | ||
| config?: ReturnType<typeof loadConfig>; | ||
| createApi?: (table: string, config: NonNullable<ReturnType<typeof loadConfig>>) => DeeplakeApi; | ||
|
|
@@ -209,7 +203,6 @@ interface ClaudePreToolDeps { | |
| readCachedIndexContentFn?: typeof readCachedIndexContent; | ||
| writeCachedIndexContentFn?: typeof writeCachedIndexContent; | ||
| writeReadCacheFileFn?: typeof writeReadCacheFile; | ||
| shellBundle?: string; | ||
| logFn?: (msg: string) => void; | ||
| } | ||
|
|
||
|
|
@@ -233,7 +226,6 @@ export async function processPreToolUse(input: PreToolUseInput, deps: ClaudePreT | |
| readCachedIndexContentFn = readCachedIndexContent, | ||
| writeCachedIndexContentFn = writeCachedIndexContent, | ||
| writeReadCacheFileFn = writeReadCacheFile, | ||
| shellBundle = SHELL_BUNDLE, | ||
| logFn = log, | ||
| } = deps; | ||
|
|
||
|
|
@@ -290,7 +282,7 @@ export async function processPreToolUse(input: PreToolUseInput, deps: ClaudePreT | |
| } | ||
|
|
||
| if (!shellCmd) return null; | ||
| if (!config) return buildFallbackDecision(shellCmd, shellBundle); | ||
| if (!config) return null; | ||
|
|
||
|
Comment on lines
284
to
286
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not fall through to the real Bash tool for accepted memory commands. Once a memory-touching Bash command has made it past Also applies to: 512-518 🤖 Prompt for AI Agents |
||
| const table = process.env["HIVEMIND_TABLE"] ?? "memory"; | ||
| const sessionsTable = process.env["HIVEMIND_SESSIONS_TABLE"] ?? "sessions"; | ||
|
|
@@ -514,10 +506,16 @@ export async function processPreToolUse(input: PreToolUseInput, deps: ClaudePreT | |
| } | ||
| } | ||
| } catch (e: any) { | ||
| logFn(`direct query failed, falling back to shell: ${e.message}`); | ||
| logFn(`direct query failed: ${e.message}`); | ||
| } | ||
|
|
||
| return buildFallbackDecision(shellCmd, shellBundle); | ||
| // No compiled handler matched (or a direct query failed). Do NOT fall | ||
| // through to the shell executor: the shell bundle runs commands via | ||
| // just-bash which may invoke real host binaries, creating a code-execution | ||
| // surface for injected patterns in unhandled commands (e.g. `sort`, `cut`). | ||
| // Return null so the original tool runs unmodified; it will fail harmlessly | ||
| // because the virtual paths do not exist on the real filesystem. | ||
| return null; | ||
| } | ||
|
|
||
| /* c8 ignore start */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isSafe()is still bypassable via shell wrappers/control syntax.This still only validates the first token of each stage, but the allowlist includes tokens that can hide a second command.
if true; then curl ~/.deeplake/memory/x; fi,timeout 1 curl ..., andfind / -exec curl ... \;all pass this check today because the dangerous command is never the first token. Please switch this to explicit validation of the exact Bash shapes you later intercept, or remove every keyword/wrapper that can dispatch a child command.Also applies to: 30-38
🤖 Prompt for AI Agents