fix(security): audit remediation — Telegram path traversal + approval-grant leak#25
Conversation
Adds FIX_PLAN.md (audit remediation tracker) and fixes the two highest- priority, lowest-risk findings: - Telegram document path traversal (Critical): DownloadDocument wrote the attacker-controlled filename through filepath.Join with no sanitization, allowing writes outside ~/.odek/media/ (e.g. ../../.ssh/authorized_keys or ../../.odek/config.json) — a remote arbitrary-write/RCE primitive. New sanitizeDocName() strips directory components and rejects degenerate names. Tests: TestSanitizeDocName, TestDownloadDocument_NoTraversal. - Batch approval grant leak (High): the approved-batch trustAll grant was reset via `defer` inside the iteration loop, so it only fired at function return — leaving trustAll set for every later iteration and auto-approving all subsequent dangerous tools. The grant is now reset at the end of each iteration's tool phase. Regression test: TestBatchApprovalTrustAllNotLeakedAcrossIterations. Remaining audit items are tracked in FIX_PLAN.md; design-sensitive ones (fail-closed Telegram auth, WS auth token) are left for maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
odek | ab00195 | Commit Preview URL Branch Preview URL |
Jun 10 2026, 10:46 AM |
Verification (vprotocol axis 2.5) found the per-iteration reset alone did not cover abnormal exits: an early return or panic within an iteration that had trustAll set could leak the grant into a later prompt, since wsApprover (per-connection) and TelegramApprover (per-chat) are reused across prompts. Restore a function-level deferred reset as a backstop — matching the safety the original defer provided — while keeping the per-iteration reset as the primary cross-iteration fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AI Verification Protocol (v5.2.7) — resultsApplied the protocol's substantive verification axes adversarially against this diff. Pipeline-orchestration scaffolding (5-agent provider diversity, certificates) was treated as out-of-scope for a self-review; the analytical axes were applied in full. Classification:
Test teeth: the trustAll regression test was confirmed to fail on the pre-fix code and pass after — it is not tautological. Verdict: findings addressed; remaining audit items tracked in |
An empty allowlist previously meant "allow all": isAllowed returned true when both AllowedChats and AllowedUsers were empty, and DefaultConfig left them empty with no warning. Anyone who found the bot could drive the agent and its shell/file tools. Now authorization is fail-closed: - ValidateConfig refuses to start when no allowlist is configured, unless the operator explicitly sets ODEK_TELEGRAM_ALLOW_ALL=true. - isAllowed denies when both allowlists are empty and AllowAllUsers is unset (defense in depth beyond the startup gate). - Startup logs a loud warning when running open via the opt-in. Adds the AllowAllUsers field + ODEK_TELEGRAM_ALLOW_ALL env var, wires it through HandlerConfig, and updates docs (TELEGRAM.md, SECURITY.md, .env.example). Existing deployments that set an allowlist are unaffected. Tests: TestValidateConfig_noAllowlistFailsClosed / _allowAllOptIn / _allowlistSatisfiesCheck, TestConfigFromEnv_allowAll, TestIsAllowed_* updated for the new semantics. Routing tests opt in via AllowAllUsers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Added: Fix #3 — Fail-closed Telegram authorization (High)
Before: an empty allowlist meant allow all — Now — fail-closed, with an explicit opt-in escape hatch:
New Verification (vprotocol axes)
Tests: Remaining open items (#4/#5 WS auth + serve-mode approval deadlock, #6 |
Challenging the #3 fix surfaced a remaining authorization bypass: HandleUpdate routes callback queries (inline-button presses) straight to handleCallback, which never called isAllowed — only handleMessage did. A characterization test (TestHandleUpdate_CallbackQueryNotAllowed) even documented the gap as "current behavior". Callbacks could therefore drive per-chat approval/clarify state and trigger outbound AnswerCallbackQuery without authorization. handleCallback now applies the same allowlist as messages, using cq.Message.Chat.ID and cq.From.ID (user 0 when From is absent). Legitimate flows are unaffected: anyone who can press a button legitimately is already allowlisted (they had to send a message to start the run). Updated the characterization test to assert the new deny behavior and added TestHandleCallback_RespectsAllowlist. Callback routing tests opt in via AllowAllUsers. A residual group-chat concern (any member can press another user's buttons when only AllowedChats is set) is noted in FIX_PLAN.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial self-review ("challenge the changes")Stress-tested this PR against its strongest objections. Results: Held up (no change needed):
Broke down → fixed in this PR (new commit):
Residual, noted not fixed (pre-existing, lower priority):
Honest cost: fail-closed is a breaking change — bots intentionally run open (no allowlist) will stop starting on upgrade until |
…leanups (#26) * fix(loop): deliver recovered tool-panic message to the LLM + review cleanups A panicking tool's recovered error message was discarded: the goroutine returned before results[idx] was assigned, so the LLM received an empty tool result and the consecutive-error tracker never counted the failure. Drop the early return so the panic message flows through like any other tool error. The existing TestToolPanic_DoesNotKillAgent never exercised this path (its message-count trigger never matched with no system prompt); switch it to a call counter and assert the panic message reaches the LLM. Review cleanups from the #25 follow-up: - loop: name the thrice-repeated anonymous interface as trustAllSetter - telegram: parse ODEK_TELEGRAM_ALLOW_ALL with strconv.ParseBool for a consistent truthy grammar (malformed values fail closed) - telegram: add TelegramConfig.HasAllowlist() and use it in both ValidateConfig and the open-bot startup warning - telegram: correct HandlerConfig allowlist comments (AllowAllUsers is only consulted when BOTH lists are empty) and document on HandleUpdate that every routed handler must enforce isAllowed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(telegram): pin strict ALLOW_ALL grammar and HasAllowlist behavior Verification-protocol remediation for #26: the claim that malformed or extended truthy values ("yes", "on", "junk") fail closed had no corresponding test, and HasAllowlist had no direct unit test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Starts work on the security/correctness audit remediation. Adds
FIX_PLAN.mdas the tracking document for all findings, and implements the two highest-priority, lowest-risk fixes (no behavior/default changes, fully tested).Fixes in this PR
1. Telegram document path traversal — Critical ✅
internal/telegram/download.go.DownloadDocumentwrote the sender-controlledDocument.FileNamethroughfilepath.Joinwith no sanitization (the variable was namedsafeNamebut nothing made it safe).filepath.Joincollapses../, so a document named../../../home/<user>/.odek/config.jsonor../../.ssh/authorized_keysescaped~/.odek/media/. Both the path and the file contents are attacker-controlled → a remote arbitrary-write / RCE primitive, reachable by anyone who can message the bot.Fix: new
sanitizeDocName()reduces the name to a single path component (filepath.Base) and rejects degenerate names ("",.,..), falling back to a deterministicdoc_<id>name.2. Batch approval
trustAllgrant leak — High ✅internal/loop/loop.go. An approved tool batch settrustAllthen reset it viadeferinside the iteration loop — so the reset only fired whenrunLoopreturned. The grant stayed active for the rest of the run, auto-approving every subsequent dangerous tool (including destructive/blocked classes). A prompt-injected model could harvest one click on a benign batch and then run anything.Fix: the grant is reset at the end of each iteration's tool-execution phase, scoped to that iteration.
Tests
TestSanitizeDocName,TestDownloadDocument_NoTraversal(traversal confined to media dir).TestBatchApprovalTrustAllNotLeakedAcrossIterations— verified to fail on the pre-fix code and pass after.go build ./...,go vet, and theinternal/loop+internal/telegramsuites all pass.Not in this PR (tracked in
FIX_PLAN.md)The remaining audit findings are documented with locations, fixes, and verification steps. Design-sensitive items are intentionally deferred for maintainer direction:
/apiauth token + serve-mode approval deadlock.~/.odek/write carve-out / shell-rc classification./newmutex race, etc.).🤖 Generated with Claude Code