You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: Resource.Search root traversal + Telegram media path allowlist
Finding #12 — Resource.Search root traversal:
- Add validateSearchQuery to reject queries containing .., path separators,
or absolute paths.
- Migrate walkAndMatch from filepath.Walk to filepath.WalkDir.
- Skip symlinked directories/files during traversal.
- Verify every match is withinRoot before returning it.
- Add regression tests for traversal and symlink skipping.
Finding #13 — Telegram media upload path traversal:
- Add internal/telegram/media_path.go with ResolveMediaPath, which restricts
outbound media to cwd, ~/.odek/media, and os.TempDir().
- Reject symlinks and paths outside the allowlist.
- Apply ResolveMediaPath in internal/telegram/handler.go sendMedia,
cmd/odek/telegram.go sendTelegramMedia, and internal/tool/send_message.go.
- send_message file argument must be absolute and pass allowlist checks.
- Add regression tests for allowed/rejected/symlink paths.
- Update docs/TELEGRAM.md, docs/SECURITY.md, and AGENTS.md.
-**@-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.
117
117
-**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.
118
118
-**Resource resolver size cap** (`internal/resource/resource.go`) — `@-resource` file loads are capped at 1 MiB to prevent OOM from `@hugefile` references.
119
-
-**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.
119
+
-**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.
120
120
-**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.
121
121
-**Tree path wrapping** (`cmd/odek/perf_tools.go`) — the `tree` tool wraps every filesystem-derived path as untrusted content.
122
122
-**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.
-**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.
139
139
-**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.
140
140
-**`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.
141
+
-**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.
141
142
-**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.
142
143
-**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.
143
144
-**Secret redaction** (`internal/redact/redact.go`) — 20+ patterns: OpenAI, Anthropic, GitHub PAT, AWS, PEM, JWT, Vault, Google OAuth, SendGrid, Discord, DB URLs, etc.
Copy file name to clipboardExpand all lines: docs/SECURITY.md
+14-3Lines changed: 14 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,7 +74,7 @@ The MCP wrapper guards a tool's **output**. The server-supplied tool **descripti
74
74
75
75
The model is instructed (via the default system prompt) to treat the wrapped region as data, not instructions. A model trained on prompt-injection resistance (Claude Sonnet 4.6+ does this well) honours the boundary. Older models or aggressively fine-tuned ones may not.
76
76
77
-
Two additional boundaries keep filesystem-derived metadata from leaking as "trusted" context. First, the `base64` tool wraps encoded output when reading from a file path, so even transformed filesystem bytes stay inside an untrusted boundary. Second, the `@`-resource resolver (`FileResolver.Search`) uses `os.Lstat` when building search-result metadata, which prevents a symlink inside the workspace from leaking the size (or other `stat` metadata) of an arbitrary target outside it.
77
+
Two additional boundaries keep filesystem-derived metadata from leaking as "trusted" context. First, the `base64` tool wraps encoded output when reading from a file path, so even transformed filesystem bytes stay inside an untrusted boundary. Second, the `@`-resource resolver (`FileResolver.Search`) rejects queries containing `..`, path separators, or absolute components before joining them with the workspace root, and uses `filepath.WalkDir` (which does not follow symlinks) for recursive autocomplete; `os.Lstat` is used when building search-result metadata, which prevents a symlink inside the workspace from leaking the size (or other `stat` metadata) of an arbitrary target outside it.
78
78
79
79
### 3. Danger classifier (shell)
80
80
@@ -324,7 +324,17 @@ The Telegram bot previously used a PID file at `~/.odek/telegram.pid` to enforce
324
324
325
325
The `send_message` tool lets the agent send inline keyboard buttons. Each button's `callback_data` is validated by the tool and again by the Telegram sender closure: any value that starts with a reserved internal prefix (`apr:`, `den:`, `trs:`, `clarify:`, `skill_save:`, `skill_skip:`) is rejected. Only user-facing `cb:` callbacks are allowed. This prevents a compromised or prompt-injected agent from presenting a button that, when clicked, would forge an approval decision or trigger a skill action.
326
326
327
-
### 23. Session ID entropy + session-scoped auth tokens
327
+
### 23. Telegram outbound media path allowlist
328
+
329
+
When the agent emits `MEDIA:photo:/path`, `MEDIA:voice:/path`, `MEDIA:document:/path`, or `send_message` with a `file`, the path is validated by `internal/telegram.ResolveMediaPath` before upload. Only paths inside an allowed base directory are permitted:
330
+
331
+
- the current working directory,
332
+
-`~/.odek/media/`, and
333
+
- the system temporary directory.
334
+
335
+
The path is resolved to an absolute, cleaned form with `filepath.Abs`, symlinks are resolved with `filepath.EvalSymlinks`, and the final component is checked with `os.Lstat`. If the final component is a symlink, or if the resolved path escapes the allowlist, the upload is rejected. This closes the arbitrary-file-read/exfiltration vector where a prompt-injected agent asks the bot to send files such as `/home/user/.ssh/id_rsa`.
336
+
337
+
### 24. Session ID entropy + session-scoped auth tokens
328
338
329
339
`odek serve` session endpoints were previously protected only by localhost binding and a short, predictable session ID (`YYYYMMDD-` + 3 random bytes ≈ 16.7 M possibilities). A local attacker who obtained IDs from `GET /api/sessions` could brute-force `GET /api/sessions/<id>` to read transcripts.
330
340
@@ -336,7 +346,7 @@ The defense has three layers:
336
346
337
347
Legacy sessions created before this defense have no `AuthToken`; the first access bootstraps one and returns it to the client, preserving backward compatibility without weakening protection for newly created sessions.
338
348
339
-
### 24. Skill and episode context wrapped as untrusted
349
+
### 25. Skill and episode context wrapped as untrusted
340
350
341
351
Skill content and retrieved session episodes are externally-sourced data that cross the trust boundary. Before injecting them as `system` messages, the loop passes them through the same nonce'd `<untrusted_content_*>` wrapper used for tool output. The skill manager already gates `NeedsReview`/tainted skills, and the memory manager filters tainted episodes from search, but the wrapper provides defense-in-depth so a compromised skill or episode cannot pose as trusted system instructions.
342
352
@@ -389,6 +399,7 @@ Defaults: `FrictionThreshold=3`, `FrictionWindow=60s`. To opt out (TTYApprover o
389
399
| Local process brute-forces session IDs to read transcripts | 128-bit IDs + session-scoped auth tokens + per-IP rate limiting |
390
400
| Telegram bot scanned by random user | Allowlist enforced before any tool call |
391
401
| Agent sends fake approval/skill button via `send_message`| Reserved internal callback prefixes rejected; only `cb:` allowed |
402
+
| Agent exfiltrates arbitrary file via Telegram media | Outbound paths restricted to cwd, `~/.odek/media/`, and temp dir; symlinks rejected |
392
403
| Auto-saved skill auto-activates on next session | Provenance gate pins NeedsReview skills to Lazy |
393
404
| Memory replays a previously-injected episode forever | Tainted episodes filtered from `Search`|
394
405
| User reflex-approves a destructive class after many benign ones | Friction mode requires typed `approve` + 1.5 s pause |
Copy file name to clipboardExpand all lines: docs/TELEGRAM.md
+9Lines changed: 9 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -206,6 +206,15 @@ All callbacks return a response string (may be empty) and an error. The `Handle`
206
206
207
207
The handler uses `sync.Map` for `TelegramApprover` instances, keyed by `chatID`. This allows the agent to send inline keyboard approval requests (yes/no) and receive responses via callback queries. The handler intercepts callback queries matching pending approval requests before dispatching to `OnCallbackQuery`.
208
208
209
+
### Outbound Media
210
+
211
+
The agent can send files back to the chat either by emitting a `MEDIA:` prefix in its final answer (`MEDIA:photo:/path`, `MEDIA:voice:/path`, `MEDIA:document:/path`) or by calling `send_message` with the `file` parameter. Before any upload, the path is validated by `internal/telegram.ResolveMediaPath`:
212
+
213
+
- Allowed directories: current working directory, `~/.odek/media/`, and the system temporary directory.
214
+
- The path is resolved to an absolute, cleaned form and checked against the allowlist.
215
+
- Symlinks are rejected: the final component is verified with `os.Lstat` and the resolved path must not escape the allowlist.
216
+
- Files outside the allowlist (e.g. `/home/user/.ssh/id_rsa`) are refused, closing prompt-injection-driven exfiltration.
0 commit comments