mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-16 16:36:53 +08:00
Three defense-in-depth fixes around untrusted input flowing to subprocess execution:
1. **Control-pane HTTP server (scripts/lib/control-pane/server.js)**
The local control-pane API binds to 127.0.0.1 but had no Host or Origin
validation, so a DNS-rebinding attack from a malicious website could pivot
into the loopback endpoints — including POST /api/actions/:id, which spawns
'cargo run -- graph ...' with caller-supplied query strings. Add a hostname
allowlist (loopback variants plus the explicitly configured --host) and
reject mismatched Host (421) or non-loopback Origin (403) before any route
handler runs.
2. **OpenCode git-summary tool (.opencode/tools/git-summary.ts)**
The tool was building 'git diff ${baseBranch}...HEAD --stat' with execSync
and a raw model-supplied baseBranch string. Switch run() to execFileSync
with an args array (no shell), validate baseBranch against a conservative
git-ref allowlist (rejects shell metacharacters, leading -, embedded ..),
and clamp the depth arg to a small positive integer before interpolating
into 'git log --oneline -<N>'.
3. **Reusable test workflow (.github/workflows/reusable-test.yml)**
The 'Install dependencies' step interpolated ${{ inputs.package-manager }}
directly into a bash 'case' and into an echo, so a downstream caller that
forwarded attacker-controllable input could inject into the runner. Move
the input into a PACKAGE_MANAGER env var and reference $PACKAGE_MANAGER
inside the script per the GitHub script-injection guidance.
Detected by Aeon + semgrep p/security-audit (host check via threat-model
manual-review axis; git-summary via detect-child-process; workflow via
run-shell-injection).
Verification: node tests/run-all.js — 2686/2687 pre-existing tests pass; the
one failure (observe.sh legacy output fallback) reproduces on main without
this branch applied. Added 2 new control-pane tests covering the allowlist
classifier and the DNS-rebinding-gate behavior end-to-end.
---
Filed by [Aeon](https://github.com/aaronjmars/aeon-aaron).
Co-authored-by: aeonframework <aeon@aaronjmars.com>
77 lines
2.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
/**
|
|
* ECC Custom Tool: Git Summary
|
|
*
|
|
* Returns branch/status/log/diff details for the active repository.
|
|
*/
|
|
|
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
|
import { execFileSync } from "child_process"
|
|
|
|
// Conservative subset of git's allowed ref-name characters. Rejects shell
|
|
// metacharacters and option-like leading `-` so a model-supplied baseBranch
|
|
// cannot inject into the shell command line built below.
|
|
const SAFE_GIT_REF = /^[A-Za-z0-9._/-]+$/
|
|
|
|
function isSafeRef(ref: string): boolean {
|
|
if (typeof ref !== "string" || ref.length === 0 || ref.length > 200) return false
|
|
if (!SAFE_GIT_REF.test(ref)) return false
|
|
if (ref.startsWith("-") || ref.startsWith(".") || ref.startsWith("/")) return false
|
|
if (ref.includes("..") || ref.includes("//")) return false
|
|
return true
|
|
}
|
|
|
|
function isSafeDepth(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 1000
|
|
}
|
|
|
|
const gitSummaryTool: ToolDefinition = tool({
|
|
description:
|
|
"Generate git summary with branch, status, recent commits, and optional diff stats.",
|
|
args: {
|
|
depth: tool.schema
|
|
.number()
|
|
.optional()
|
|
.describe("Number of recent commits to include (default: 5)"),
|
|
includeDiff: tool.schema
|
|
.boolean()
|
|
.optional()
|
|
.describe("Include diff stats against base branch (default: true)"),
|
|
baseBranch: tool.schema
|
|
.string()
|
|
.optional()
|
|
.describe("Base branch for diff comparison (default: main)"),
|
|
},
|
|
async execute(args, context) {
|
|
const cwd = context.worktree || context.directory
|
|
const depth = isSafeDepth(args.depth) ? args.depth : 5
|
|
const includeDiff = args.includeDiff ?? true
|
|
const baseBranch = args.baseBranch ?? "main"
|
|
|
|
const result: Record<string, string> = {
|
|
branch: runArgs(["branch", "--show-current"], cwd) || "unknown",
|
|
status: runArgs(["status", "--short"], cwd) || "clean",
|
|
log: runArgs(["log", "--oneline", `-${depth}`], cwd) || "no commits found",
|
|
}
|
|
|
|
if (includeDiff) {
|
|
result.stagedDiff = runArgs(["diff", "--cached", "--stat"], cwd) || ""
|
|
result.branchDiff = isSafeRef(baseBranch)
|
|
? runArgs(["diff", `${baseBranch}...HEAD`, "--stat"], cwd) ||
|
|
`unable to diff against ${baseBranch}`
|
|
: `unable to diff against ${baseBranch} (invalid ref)`
|
|
}
|
|
|
|
return JSON.stringify(result)
|
|
},
|
|
})
|
|
|
|
export default gitSummaryTool
|
|
|
|
function runArgs(args: string[], cwd: string): string {
|
|
try {
|
|
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
} catch {
|
|
return ""
|
|
}
|
|
}
|