Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6557686
security: parallel_shell approval fallback + Telegram IPI hardening +…
jkyberneees Jun 13, 2026
ce35e0d
security: wrap parallel_shell stdout/stderr as untrusted
jkyberneees Jun 13, 2026
af28c55
FIX #7: resolve symlink directories before classifying read-only tools
jkyberneees Jun 13, 2026
9cb9ad4
FIX #8: @-resource resolver follows intermediate symlink directories
jkyberneees Jun 13, 2026
b9493af
FIX #9: apply SSRF transport guard to web_search
jkyberneees Jun 13, 2026
78e3648
FIX #10: classify git clone/fetch/pull as NetworkEgress
jkyberneees Jun 13, 2026
0cd5a53
FIX #11: classify script interpreters and package-manager run/build a…
jkyberneees Jun 13, 2026
ebf45c1
FIX #12: protect ~/.odek/IDENTITY.md and scan it before trusting as s…
jkyberneees Jun 13, 2026
61795eb
FIX #13: wrap MCP tool descriptions as untrusted data, not just regex…
jkyberneees Jun 13, 2026
177e334
FIX #14: extract telegramVoiceMessage helper + regression test for tr…
jkyberneees Jun 13, 2026
658c981
Harden Telegram scheduled tasks: operator-only /schedule management, …
jkyberneees Jun 14, 2026
7668b2c
Restrict /restart to operator chats/users and add cooldown
jkyberneees Jun 14, 2026
0594fe6
Document /restart operator gating in Docker setup
jkyberneees Jun 14, 2026
2237970
Cap Telegram file downloads: 5 MiB default + optional per-chat quota
jkyberneees Jun 14, 2026
2163c3b
Enforce Telegram MaxMsgLength for text and captions
jkyberneees Jun 14, 2026
2b7d91e
Wrap patch tool diff output as untrusted content
jkyberneees Jun 14, 2026
d7e840c
security: wrap delegate_tasks summary and Telegram photo captions as …
jkyberneees Jun 14, 2026
102a2f4
Harden audit divergence heuristic against response-only exfiltration …
jkyberneees Jun 14, 2026
eec7854
Harden prompt-injection blacklist scanners
jkyberneees Jun 14, 2026
eea12a9
Delete Windows sub-agent API-key temp file after use
jkyberneees Jun 14, 2026
8719bcd
Fix daily token budget file permissions and race
jkyberneees Jun 14, 2026
f5691cc
security: validate Telegram fallback URLs to prevent token leak
jkyberneees Jun 14, 2026
d6895c6
security: require explicit approval for project-level MCP servers
jkyberneees Jun 14, 2026
5f6ba39
security: confine sandbox volume mounts to working directory
jkyberneees Jun 14, 2026
63286dc
fix: enforce --sandbox-readonly for write_file, patch, batch_patch
jkyberneees Jun 14, 2026
02beff2
fix: reject base_url from project config
jkyberneees Jun 14, 2026
06842e1
fix: disallow api_key, system, dangerous from project config
jkyberneees Jun 14, 2026
d384e26
fix: sanitise MCP server subprocess environment
jkyberneees Jun 14, 2026
623b511
fix: harden schedule atomic writes against symlink attacks
jkyberneees Jun 14, 2026
d360a82
fix: replace telegram PID-file singleton lock with flock
jkyberneees Jun 14, 2026
01bfc9c
fix: session ID entropy + auth tokens; restrict send_message callbacks
jkyberneees Jun 14, 2026
e187f8d
fix: wrap skill/episode context as untrusted before injection
jkyberneees Jun 14, 2026
c8e97af
fix: Resource.Search root traversal + Telegram media path allowlist
jkyberneees Jun 14, 2026
2013bd7
fix: validate session/episode vector index IDs and reject symlinks
jkyberneees Jun 14, 2026
56be5d3
fix: address verification-protocol findings on security branch
jkyberneees Jun 14, 2026
5bdb456
fix: skip empty untrusted-content sources in audit aggregation
jkyberneees Jun 14, 2026
e581503
fix: address remaining security findings on sandbox confinement and a…
jkyberneees Jun 14, 2026
7f5a076
chore: add golangci-lint config and fix resource lint issue
jkyberneees Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ docker/.odek/*

# Claude Code local artifacts
.claude/

sec_findings.md
178 changes: 178 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# golangci-lint configuration for odek.
#
# Goal: enable a core set of linters in CI without requiring a one-shot
# refactor of the whole codebase. Test files are excluded from the noisiest
# rules (errcheck, unused) because setup helpers frequently ignore errors or
# are conditionally compiled. Pre-existing production issues are listed below
# with explicit excludes so CI stays green; they should be removed as the code
# is touched.
run:
timeout: 10m
go: "1.24"

linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused

linters-settings:
errcheck:
# Ignore common error-delegation patterns where the error is handled
# implicitly (e.g. fmt.Fprintf to stderr).
exclude-functions:
- fmt.Fprintf
- fmt.Fprintln
- io.WriteString

issues:
exclude-rules:
# Test helpers commonly ignore setup errors, may define conditionally unused
# helpers, and often contain intentionally empty branches / identical
# comparison assertions. Keep govet enabled for tests; silence the rest.
- path: _test\.go
linters:
- errcheck
- gosimple
- ineffassign
- staticcheck
- unused

# ------------------------------------------------------------------
# Pre-existing production issues to be fixed incrementally. Each entry
# points to a concrete lint finding that existed before golangci-lint
# was enabled in CI. Contributors should remove the matching exclude
# when fixing the underlying code.
# ------------------------------------------------------------------

# cmd/odek/file_tool.go: unchecked filepath.Walk errors.
- path: cmd/odek/file_tool.go
linters:
- errcheck

# cmd/odek/main.go: unchecked fmt.Sscanf/Scanf/store.Save/sl.RecordSkip.
- path: cmd/odek/main.go
linters:
- errcheck

# cmd/odek/mcp.go: unchecked error return.
- path: cmd/odek/mcp.go
linters:
- errcheck

# cmd/odek/perf_tools.go: unchecked Walk/Seek/io.Copy/Process.Kill.
- path: cmd/odek/perf_tools.go
linters:
- errcheck

# cmd/odek/repl.go: unchecked store.Save.
- path: cmd/odek/repl.go
linters:
- errcheck

# cmd/odek/repl_editor.go: unchecked terminal restore/read.
- path: cmd/odek/repl_editor.go
linters:
- errcheck

# cmd/odek/serve.go: unchecked error returns + unused wsStreamWriter.
- path: cmd/odek/serve.go
linters:
- errcheck
- unused

# cmd/odek/telegram.go: unchecked bot.* and os.MkdirAll error returns.
- path: cmd/odek/telegram.go
linters:
- errcheck

# cmd/odek/wsapprover.go: unchecked rand.Read error.
- path: cmd/odek/wsapprover.go
linters:
- errcheck

# cmd/odek/subagent.go: unchecked enc.Encode.
- path: cmd/odek/subagent.go
linters:
- errcheck

# cmd/odek/vision_tool.go: unchecked fmt.Sscanf.
- path: cmd/odek/vision_tool.go
linters:
- errcheck

# internal/memory/memory.go: unchecked episode write.
- path: internal/memory/memory.go
linters:
- errcheck

# internal/skills/cache.go/tools.go/types.go: unchecked Rename/Unmarshal.
- path: internal/skills/cache.go
linters:
- errcheck
- path: internal/skills/tools.go
linters:
- errcheck
- path: internal/skills/types.go
linters:
- errcheck

# internal/flock/flock.go: unchecked unlockFile.
- path: internal/flock/flock.go
linters:
- errcheck

# internal/telegram/approver.go: unchecked EditMessageText/rand.Read.
- path: internal/telegram/approver.go
linters:
- errcheck

# internal/telegram/health.go: unchecked json.Encode.
- path: internal/telegram/health.go
linters:
- errcheck

# internal/llm/client.go: unused var + empty branch.
- path: internal/llm/client.go
linters:
- unused
- staticcheck

# internal/loop/loop.go: unnecessary fmt.Sprintf.
- path: internal/loop/loop.go
linters:
- gosimple

# internal/mcpclient/client.go: unused type/fields/function + unchecked Wait/Kill.
- path: internal/mcpclient/client.go
linters:
- errcheck
- unused

# internal/memory/buffer.go: unnecessary fmt.Sprintf.
- path: internal/memory/buffer.go
linters:
- gosimple

# internal/memory/memory.go: unused const.
- path: internal/memory/memory.go
linters:
- unused

# internal/memory/merge.go: ineffectual assignments.
- path: internal/memory/merge.go
linters:
- ineffassign

# internal/telegram/bot.go: deprecated netErr.Temporary.
- path: internal/telegram/bot.go
linters:
- staticcheck

# internal/schedule/store.go: unchecked syscall.Flock.
- path: internal/schedule/store.go
linters:
- errcheck
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ System prompt is loaded by priority: `--system` flag > `~/.odek/IDENTITY.md` > c
Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECURITY.md](docs/SECURITY.md).

- **Untrusted-content wrapper** (`cmd/odek/untrusted.go`) — every tool whose output sources from outside the trust boundary (`browser`, `read_file`, `shell`, `search_files`, `multi_grep`, `transcribe`, `head_tail`, `diff`, `tr`, `sort`, `json_query`, `batch_patch`, `glob`, `file_info`, `tree`, `base64` file mode, `session_search`, `@-resources`, `--ctx` files, any MCP tool) wraps results in `<untrusted_content_<nonce> source="...">…</untrusted_content_<nonce>>`. Browser page title and interactive-element text are wrapped in addition to the main content. Per-call nonce defeats wrapper-escape via literal close tag.
- **Audit log** (`cmd/odek/audit.go` + `internal/session/audit.go`) — every `wrapUntrusted` call records source + content-hash + turn into `<sessions>/audit/<id>.json`. After each turn a divergence heuristic flags `suspicious_divergence=true` when the agent ingested untrusted content AND its tool calls referenced resources the user did not mention. Inspect with `odek audit <session-id>` / `odek audit --list`.
- **Audit log** (`cmd/odek/audit.go` + `internal/session/audit.go`) — every `wrapUntrusted` call records source + content-hash + turn into `<sessions>/audit/<id>.json`. After each turn a divergence heuristic flags `suspicious_divergence=true` when the agent ingested untrusted content AND its actions or final response reference resources that either did not appear in the user's message or were introduced by the untrusted content itself (closing response-only exfiltration and reused-resource injection bypasses). Inspect with `odek audit <session-id>` / `odek audit --list`.
- **Memory taint** (`internal/memory/provenance.go`) — `EpisodeProvenance` tracks Untrusted/Sources/UserApproved. Tainted episodes are stored but `Search()` filters them out, so a one-shot injection cannot persist via the episode pipeline. User must explicitly promote.
- **Skill provenance gate** (`internal/skills/loader.go` + `cache.go`) — `Skill.Provenance{Untrusted, Sources, NeedsReview}`. NeedsReview skills pin to Lazy regardless of `auto_load`. `odek skill promote <name>` clears the flag after user review.
- **Sub-agent damage cap** (`cmd/odek/subagent.go::applySubagentTrust`) — `delegate_tasks` carries `trust_level` + `max_risk`. Untrusted ⇒ NonInteractive=deny, Destructive/CodeExec/Install/SystemWrite/NetworkEgress all forced to Deny. `max_risk` ⇒ everything above cap forced to Deny.
Expand All @@ -113,11 +113,12 @@ Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECU
- **glob tool hardening** (`cmd/odek/file_tool.go`) — `glob` caps results at 1,000 matches and wraps returned paths as untrusted content.
- **Sub-agent task-file cap** (`cmd/odek/subagent.go`) — `odek subagent --task <file>` rejects task files larger than 10 MiB before loading them into memory.
- **session_search hardening** (`cmd/odek/session_search_tool.go`) — the `get` action returns at most the 100 most recent messages and wraps each message content, task, and buffer entry as untrusted; `list`/`search`/`find` also wrap session tasks.
- **Session vector index hardening** (`internal/session/vector_index.go`) — `rebuildLocked` validates every session filename with `ValidateSessionID` and skips symlinks via `DirEntry.Type()` and `os.Lstat`, preventing a planted symlink from embedding arbitrary files into the semantic search corpus.
- **@-resource / --ctx prompt wrapping** (`cmd/odek/refs.go`, `cmd/odek/serve.go`) — content resolved from `@file` references and `--ctx` files is wrapped as untrusted before being inserted into the prompt.
- **Config file size cap** (`internal/config/loader.go`) — `~/.odek/config.json` and `./odek.json` are rejected if larger than 5 MiB to prevent OOM from a malicious or broken config at startup.
- **Resource resolver size cap** (`internal/resource/resource.go`) — `@-resource` file loads are capped at 1 MiB to prevent OOM from `@hugefile` references.
- **Resource resolver symlink hardening** (`internal/resource/resource.go`) — `FileResolver.Search` uses `os.Lstat` (not `os.Stat`) for search-result metadata, so symlinks cannot leak the size of arbitrary targets outside the workspace.
- **Sub-agent summary cap** (`cmd/odek/subagent_tool.go`) — each sub-agent result included in the `delegate_tasks` summary is truncated to 100 KiB to prevent memory DoS.
- **Resource resolver search hardening** (`internal/resource/resource.go`) — `FileResolver.Search` rejects queries containing `..`, path separators, or absolute components before joining them with the workspace root, and uses `filepath.WalkDir` so directory symlinks are not followed during recursive autocomplete. `os.Lstat` (not `os.Stat`) is used for search-result metadata, so symlinks cannot leak the size of arbitrary targets outside the workspace.
- **Sub-agent summary cap + wrapping** (`cmd/odek/subagent_tool.go`) — each sub-agent result included in the `delegate_tasks` summary is truncated to 100 KiB to prevent memory DoS, and the final aggregated summary is wrapped as untrusted content so a compromised sub-agent cannot inject instructions into the parent context.
- **Tree path wrapping** (`cmd/odek/perf_tools.go`) — the `tree` tool wraps every filesystem-derived path as untrusted content.
- **head_tail output cap** (`cmd/odek/perf_tools.go`) — `head_tail` truncates returned lines so total content stays within 1 MiB, preventing multi-file/multi-line memory DoS.
- **search_files symlink hardening** (`cmd/odek/file_tool.go`) — the `files` target uses `Lstat` (not `Stat`) and skips symlinks in the glob branch, closing metadata disclosure via symlinked paths.
Expand All @@ -130,6 +131,18 @@ Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECU
- **Session file size cap** (`internal/session/session.go`) — session files larger than 32 MiB are rejected by `Load()` to prevent OOM from tampered or corrupted transcripts.
- **Skill file size cap** (`internal/skills/loader.go`) — `SKILL.md` files larger than 1 MiB are skipped so a malicious project cannot OOM the process at startup or bloat the system prompt.
- **Serve sandbox default-on** — `odek serve` enables `--sandbox` automatically unless `--no-sandbox` is passed.
- **Sandbox volume confinement** (`internal/sandbox/sandbox.go`) — extra `--sandbox-volume` host paths must resolve to a location under the working directory, cannot contain `..` or symlink escapes, and cannot match sensitive prefixes such as `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/home`, `/var`, `/run`, or `/var/run/docker.sock`.
- **Sandbox read-only enforcement** (`cmd/odek/sandbox_file.go` + `cmd/odek/file_tool.go` + `cmd/odek/perf_tools.go`) — when a sandbox container is active, `write_file`, `patch`, and `batch_patch` translate host paths to `/workspace/...` and copy data into the container with `docker cp`, so a read-only workspace mount (`--sandbox-readonly`) is enforced for the agent's own file tools.
- **Project config sensitive-field rejection** (`internal/config/loader.go`) — `./odek.json` is untrusted, so `base_url`, `api_key`, `system`, and the `dangerous` section set there are ignored (with stderr warnings). These can only be configured from operator-controlled sources: `~/.odek/config.json`, `ODEK_*` env vars, or CLI flags.
- **MCP subprocess environment sanitisation** (`internal/mcpclient/client.go`) — MCP server children receive only a minimal allowlist of safe environment variables plus explicit `env` overrides. Keys matching secret patterns (`*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, etc.) are stripped, preventing a compromised or malicious MCP server from reading parent secrets.
- **Schedule atomic-write hardening** (`internal/schedule/store.go` + `internal/fsatomic`) — schedule file writes now use `fsatomic.WriteFile`, which creates a random temp file with `O_EXCL`, fsyncs data and directory, and renames over the target. A swapped-in symlink is replaced rather than followed, closing the symlink-override attack on `schedules.json` / `schedule-state.json`.
- **Telegram singleton flock lock** (`cmd/odek/telegram.go` + `internal/flock`) — the Telegram bot now uses an advisory `flock` on `~/.odek/telegram.lock` instead of a PID file probed with signals. This removes the non-Linux path where a planted PID could cause odek to kill an arbitrary process.
- **Telegram photo caption wrapping** (`cmd/odek/telegram.go`) — photo captions cross the Telegram trust boundary, so they are wrapped as untrusted content both when passed to the local vision model and when injected into the main agent's user message.
- **`send_message` callback prefix restriction** (`internal/tool/send_message.go` + `cmd/odek/telegram.go`) — the `send_message` tool rejects any button whose `callback_data` starts with a reserved internal prefix (`apr:`, `den:`, `trs:`, `clarify:`, `skill_save:`, `skill_skip:`); only user-facing `cb:` callbacks are allowed. The Telegram sender closure validates again as defense-in-depth, preventing a forged approval or skill button.
- **Telegram outbound media path allowlist** (`internal/telegram/media_path.go` + `internal/telegram/handler.go` + `internal/tool/send_message.go` + `cmd/odek/telegram.go`) — paths supplied to `MEDIA:...` prefixes or `send_message(file=...)` are resolved to an absolute path and verified against an allowlist (cwd, `~/.odek/media/`, system temp dir). `os.Lstat` rejects symlink final components and `filepath.EvalSymlinks` ensures the resolved path does not escape the allowlist, preventing prompt-injection-driven exfiltration of arbitrary files.
- **Session ID entropy + session-scoped auth tokens** (`internal/session/session.go`, `cmd/odek/serve.go`) — session IDs now carry 128 bits of randomness (16 bytes / 32 hex chars); each session stores a 256-bit `AuthToken` required by `GET/DELETE/POST /api/sessions/<id>` and WebSocket session-resume messages via `X-Session-Token` header, `session_token` cookie, or `auth_token` WS field. Per-IP rate limiting (60/min) on session lookups adds a brute-force backstop.
- **Skill/episode untrusted wrapper** (`internal/loop/loop.go` + `odek.go`) — skill context and retrieved session-episode context are passed through the caller-provided untrusted wrapper (the same nonce'd `<untrusted_content_*>` boundary used for tool output) before being injected into the model's system context. This prevents a compromised or tainted skill/episode from being treated as trusted system instructions.
- **Episode index session ID validation** (`internal/memory/episode_index.go` + `internal/session/session.go`) — `readAllSummaries` treats `index.json` as untrusted input and validates every `session_id` with `session.ValidateSessionID` before building the `filepath.Join(dir, sessionID+".md")` path. Invalid / traversal / separator-containing IDs are skipped with a warning, preventing a tampered episode index from pulling arbitrary files (e.g. `~/.odek/config.json`, `IDENTITY.md`) into the embedding space.
- **Secret redaction** (`internal/redact/redact.go`) — 20+ patterns: OpenAI, Anthropic, GitHub PAT, AWS, PEM, JWT, Vault, Google OAuth, SendGrid, Discord, DB URLs, etc.

### Platform Support
Expand Down
Loading
Loading