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(serve): bootstrap session tokens for instance-token-authenticated front-ends
Sessions created by one front-end (e.g. bodek, or the WebUI in another
browser profile) could not be loaded by another: every new session gets a
256-bit AuthToken that only reaches the creating client, and the token
bootstrap only covered legacy sessions with no token at all - so
GET /api/sessions/<id> returned 401 and the WebUI showed "Failed to load
session".
The GET handler now also bootstraps the session token when the caller
proves knowledge of the per-instance CSRF token by presenting it in the
X-Odek-Ws-Token header (constant-time compared). Header presentation is a
knowledge proof a cross-origin page cannot forge: a DNS-rebinding page
holds only the ambient SameSite=Strict cookie and can neither read the
token value nor set the custom header (CORS preflight, which odek does
not answer). Cookie-only callers therefore still get 401 on
token-carrying sessions, while the operator's legitimate front-ends -
which always send the header via apiHeaders - can load each other's
sessions. The rebinding damage boundary is unchanged; DELETE/POST
(rename) flows inherit the fix through the client's existing
ensureSessionToken GET bootstrap.
Regression tests cover: bootstrap with a valid instance header (200 +
X-Session-Token), no bootstrap with a wrong instance header (401), and no
bootstrap for cookie-only callers (401). Docs updated (SECURITY.md
section 24, AGENTS.md).
-**Memory add/replace pipe-to-shell filter** (`internal/memory/memory.go`) — `AddFact` and `ReplaceFact` now run `FactLooksUnsafe` in addition to the general guard scan, blocking agent-driven planting of download-and-execute facts.
200
200
-**Skill learn-loop provenance propagation** (`internal/skills/learnloop.go`) — conversation-extracted suggestions and LLM-enhanced suggestions both retain the session's `SkillProvenance`, so tainted sessions cannot produce clean-looking auto-saved skills.
201
201
-**Audit ingest recording for @-refs, `--ctx`, and Web-UI attachments** (`cmd/odek/refs.go`, `cmd/odek/serve.go`, `cmd/odek/audit.go`) — the per-session ingest recorder is attached before `@`-reference/`--ctx`/attachment resolution, `recordTurnAudit` scans `user` messages for untrusted wrappers in addition to `tool` messages, and the divergence heuristic compares agent actions against the original pre-enrichment prompt so attacker-injected resources are not treated as user-mentioned.
202
-
-**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>`, `POST /api/cancel`, 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. `X-Forwarded-For` / `X-Real-Ip` headers are only honored when the direct remote address is in the configured `trusted_proxies` list, so spoofed headers cannot bypass the limiters.
202
+
-**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>`, `POST /api/cancel`, 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. `X-Forwarded-For` / `X-Real-Ip` headers are only honored when the direct remote address is in the configured `trusted_proxies` list, so spoofed headers cannot bypass the limiters. Callers that present the per-instance CSRF token in the `X-Odek-Ws-Token` header (a knowledge proof a cross-origin page cannot forge) are bootstrapped the session token on `GET`, so the operator's front-ends (WebUI, bodek) can load each other's sessions; cookie-only callers still get 401.
203
203
-**Skill/episode untrusted wrapper** (`internal/loop/loop.go` + `odek.go`) — skill context (both auto-load and lazy trigger-matched), the `skill_load` tool output, 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.
204
204
-**`env` / `printenv` environment-dump gate** (`internal/danger/classifier.go`) — bare `env` and `printenv` invocations are classified as `system_write` because they can leak process-environment secrets that the redaction scanner does not recognise. `env VAR=value <cmd>` still classifies `<cmd>` normally.
205
205
-**Web UI attachment wrapping** (`cmd/odek/serve.go` + `cmd/odek/ui/app.js`) — files attached through the browser are sent separately from the user's text and wrapped with the nonce'd `<untrusted_content_*>` boundary (`source="attachment:<filename>"`) before injection into the prompt.
Copy file name to clipboardExpand all lines: docs/SECURITY.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -467,6 +467,8 @@ The IP used for rate limiting is taken from `X-Forwarded-For` / `X-Real-Ip` only
467
467
468
468
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.
469
469
470
+
**Cross-front-end bootstrap (header knowledge proof).** A session created by one front-end (e.g. bodek, or the WebUI in another browser profile) carries an `AuthToken` the other front-ends don't have, so it could not be loaded by them. `GET /api/sessions/<id>` now also bootstraps the session token when the caller proves **knowledge of the per-instance CSRF token** by presenting it in the `X-Odek-Ws-Token` header (constant-time compared). Header presentation is a knowledge proof a cross-origin page cannot forge: a DNS-rebinding page holds only the ambient `SameSite=Strict` cookie and can neither read the token value nor set the custom header (it triggers a CORS preflight odek does not answer). Cookie-only callers therefore still get 401 on token-carrying sessions, while the operator's legitimate front-ends — which always send the header — can load each other's sessions. The rebinding damage boundary is unchanged.
471
+
470
472
### 25. Skill and episode context wrapped as untrusted
471
473
472
474
Skill content and retrieved session episodes are externally-sourced data that cross the trust boundary. Before injecting them as `system` messages, the auto-load path (`odek.go`) and the lazy-match path (the loop) pass them through the same nonce'd `<untrusted_content_*>` wrapper used for tool output; the `skill_load` tool output is wrapped the same way at registration. 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.
0 commit comments