fix(security): harden online hosting defaults#169
Conversation
|
I checked this against #598 because both PRs introduce They overlap in the shared URL-security helper but appear to have different scopes:
Because both add No action required from my side right now — just flagging the overlap so the two security-hardening paths stay aligned. |
fb1bed3 to
84039b7
Compare
ccce198 to
cc5b569
Compare
|
Rebased this PR onto current dev and resolved the merge conflict in For the #598 overlap: Also updated the PR title to Conventional Commits form: Verification after rebase:
The regular CI workflow is still awaiting maintainer approval for the fork run. |
cc5b569 to
65cca48
Compare
alteixeira20
left a comment
There was a problem hiding this comment.
I checked this again, and I still can’t approve it yet. The PR is going in the right direction, but I see three blockers.
- First-run setup can be bypassed with spoofed forwarding headers.
_request_from_loopback() trusts X-Forwarded-For / X-Real-IP and then treats that value as the client host. That means a remote request can spoof a loopback value in forwarding headers and pass the local-only /api/auth/setup check.
Please don’t trust forwarded headers unless the immediate peer is a configured trusted proxy. The simplest fix here is probably to base first-run setup locality on request.client.host only, or add an explicit trusted-proxy allowlist before honoring forwarded headers. Please also add regression tests for remote clients spoofing loopback through X-Forwarded-For and X-Real-IP.
- The new API-token middleware gate looks too narrow.
_api_token_route_allowed() only allows POST /api/v1/chat with the chat scope. That means valid bearer tokens for other token-aware routes, such as Codex/email/calendar/memory/documents routes, can be rejected by middleware before those routes get to enforce their own scopes.
Please either extend this gate to cover the existing token-aware route families and scopes, or avoid duplicating route-scope policy in middleware so the route-level checks still run. A regression test for at least one non-chat scoped token route would be useful here.
action_daily_briefstill opens IMAP without owner scope.
Nearby email actions were updated to pass owner, but the daily brief path still calls _imap_connect(None). In a multi-user setup this can select another user’s default mailbox.
Please pass the task owner into that connection and add a regression test for the daily brief path.
The direction is good, but these are security/owner-scope issues, so I’d rather see them fixed before merge.
d86f87b to
44623cd
Compare
|
I pushed a maintainer rescue/rebase directly to this PR branch. What I changed while rescuing it onto current
Validation run locally:
This should now be ready for a fresh maintainer review. |
o3LL
left a comment
There was a problem hiding this comment.
Review
The genuinely-wired parts of this PR are fine, but the headline "hardening" — the URL/SSH/port validators and the attachment path-traversal protection — is dead code that no production path calls. The tests pass only because they invoke the functions directly, which gives a false sense of coverage. Requesting changes.
What actually works (keep these)
- First-run setup gating (
routes/auth_routes.py).loopback OR X-Odysseus-Setup-Token OR ODYSSEUS_ALLOW_REMOTE_SETUPis correctly wired intoPOST /api/auth/setup._request_from_loopbackreadsrequest.client.host(the real TCP peer), not a spoofable header — good, and the regression test confirms it ignoresX-Forwarded-For/X-Real-IP. - Email
ownerscoping (email_pollers.py+builtin_actions.py). Threaded through cleanly;owner=""default preserves existing behavior, and_imap_connect/_auto_summarize_*signatures all accept it. No breakage. - Docs/env additions.
SECURE_COOKIES,ODYSSEUS_ALLOW_PRIVATE_CALDAV, andEMBEDDING_BLOCK_PRIVATE_IPSare all read by pre-existing code (auth_routes.py,src/caldav_sync.py,routes/embedding_routes.py), so.env.example/.env.online.example/docs/ONLINE_HOSTING.mdaccurately document real features. Useful.
Critical: the new validators are never called
Grepping the head branch for call sites (excluding definitions and tests) returns nothing for every one of these:
src/url_security.py:validate_http_url,validate_ssh_destination,validate_tcp_port,private_integration_urls_allowed,private_model_endpoints_allowed,_reject_private_host,_resolve_addressesroutes/email_helpers.py:_safe_attachment_target_dir
Concretely:
- The attachment path-traversal fix does nothing —
_safe_attachment_target_diris defined but the actual attachment-extraction code is untouched, so the claimed protection isn't in any request path. - The SSH/port validators are both dead and duplicative.
routes/shell_routes.py:_ssh_base_argvalready validates host (rejects a leading-) and port inline, and builds anargvlist (no shell). Meanwhile the genuinely risky site —routes/cookbook_helpers.py:_ssh, which does raw f-stringf"ssh {pf}{host} '{cmd}'"shell interpolation — is not guarded by the new validator.
The PR description asserts validation "is retained ... for trusted/admin-controlled integrations, SSH destinations, TCP ports, ... attachment extraction paths." As submitted, none of that is connected. Please either wire these into their real call sites (and show the before/after at the actual site), or drop them — shipping unreferenced validators plus green unit tests reads as coverage that doesn't exist.
Medium: loopback gate is defeated by this PR's own recommended topology
_request_from_loopback trusts request.client.host == 127.0.0.1. But docs/ONLINE_HOSTING.md recommends fronting Odysseus with Caddy/nginx/Tailscale on the same Mac — in which case the reverse proxy connects from 127.0.0.1 for every external visitor, so the loopback branch auto-allows public /setup access. The only thing saving you is the is_configured race window before the first admin exists. The docs say "prefer opening /setup on the Mac itself," but the loopback auto-allow silently undermines the token path they steer users toward. At minimum, document that behind a same-host proxy the loopback gate is meaningless and the token is mandatory; better, gate on a configured trusted-proxy allowlist.
Minor
validate_http_urlresolves DNS to checkis_globalbut returns the original unmodified URL — a future caller re-resolves, opening a TOCTOU / DNS-rebinding gap. Harmless while the function is dead, but a trap for whoever wires it later assuming it's safe.- The PR checklist claims "no unrelated ... whitespace changes," yet
.dockerignore/.gitignorerewrite—→-in comments — unrelated churn. _safe_attachment_target_dirdoesimport re as _reinside the function body rather than at module scope.
|
I checked the latest head and the current requested-changes review from o3LL. and I agree with the already-posted dead-helper and proxy-topology concerns, also I found one additional owner-boundary issue plus one docs/config scope issue below. Existing Review Contexto3LL's current-head requested-changes review is correct that the newly added URL/SSH/port validators and attachment helper are not called by production code, and that the same-host proxy topology in the new online-hosting guidance defeats the setup route's loopback trust. I am not repeating those as full findings here; this review adds the non-duplicative account-id owner branch and the setup-env/docs scope issue. Findings
Validation
|
44623cd to
4aeea43
Compare
|
Rescue completed and pushed as The PR was rebased onto current What changed:
Validation:
CI is now running against the rescued head. |
|
Reviewed the rescued head ( Nothing below is a blocker. 1. (Medium, operational) No bootstrap escape hatch for headless deployments. After this change 2. (Low, inherent limitation) 3. (Low, style) New test files use tab indentation while the codebase uses 4-space indent. This will trip flake8 4. (Low, test coverage) Only the negative path is covered. The owner-scope test asserts rejection of a foreign 5. (Nit) Zero-account owner falls back to legacy config. In Verdict: good shape and a real multi-tenant isolation improvement. Suggested before merge: the tab→spaces fix (.3, CI) and the documented headless-bootstrap workaround (.1); ideally the positive/fan-out tests (.4). .2 and .5 are acknowledge-or-defer. |
|
@o3LL Applied the actionable follow-up suggestions in Changes made:
I intentionally left the zero-account legacy fallback unchanged. It remains owner-scoped and preserves existing settings.json-based single-user installations. Changing that behavior should be handled as a separate compatibility decision rather than as part of this security follow-up. Validation completed before push:
The new CI run on this head will provide the final full-suite result. |
|
Thanks, is the failing CI related to those changes ? Could you fix it before I do a last review ? |
|
Fixed the full-suite CI failure in The hardened first-run locality check correctly evaluates the direct client, request headers, and requested hostname. Three older password-policy tests still used partial fake requests containing only I updated only those three fixtures to represent complete direct-localhost requests. The production security check was not weakened. Validation before push:
|
|
Re-reviewed at
I also checked the CI fix in LGTM — this is good to merge from my side. |
RaresKeY
left a comment
There was a problem hiding this comment.
Thanks for the update rescue. I checked the latest head and found two remaining issues in the first-run setup and scheduled-email owner-boundary changes.
Findings
issue (security): Reject Cloudflare tunnel metadata during first-run setup
-
Problem: The new direct-loopback setup check rejects common forwarding headers, but it omits the Cloudflare headers that the app's existing trusted-loopback policy already treats as proxy/tunnel evidence. A request arriving from a local cloudflared upstream with loopback peer and host values can therefore pass setup while carrying
CF-Connecting-IP. -
Impact: Before an administrator exists, a remote request forwarded through that tunnel/proxy shape can create the first administrator without being a direct local request.
-
Ask: Share the existing proxy-header policy or reject the Cloudflare headers here too, and add a regression for this header shape.
-
Location:
routes/auth_routes.py:88,routes/auth_routes.py:122
issue (compatibility): Preserve claimable legacy mailboxes in task owner scoping
-
Problem: The new task fan-out and explicit-account guard require an exact account-row owner match. The established email-account policy also allows an ownerless legacy row when its mailbox address matches the caller. As a result, a scheduled task can omit or reject its owner's matching imported legacy mailbox.
-
Impact: Existing scheduled summary/reply/calendar tasks can stop processing a mailbox that remains valid and accessible to its owner elsewhere in the email subsystem.
-
Ask: Reuse the owner-or-matching-legacy visibility policy for explicit task IDs and fan-out, while still rejecting foreign accounts. Add mixed owned/legacy and explicit matching-legacy regressions.
-
Location:
routes/email_pollers.py:197,routes/email_pollers.py:263
Validation
- Focused tests for the changed setup, email-task, and setup-policy paths passed locally.
- The Cloudflare-header case is source-confirmed in a simulated isolated check; live proxy/tunnel and real IMAP validation were not run.
|
Addressed the two remaining findings in First-run setup locality
Scheduled email account ownership
Validation
The two reported findings should now be resolved. Please take another look when convenient. |
Summary
Refreshes the online-hosting hardening PR onto current
devwhile keeping the merged #598 URL-security API intact. Token-supplied chatbase_urlremains public-only; broader validation is retained only for trusted/admin-controlled integrations, SSH destinations, TCP ports, email ownership/cache boundaries, attachment extraction paths, first-run setup defaults, and online hosting env/docs guidance.Target branch
dev, notmain. All PRs land indev;mainis curated by the maintainer at each release. If your PR is onmainby accident, click "Edit" on this PR and change the base.Linked Issue
Related to #598 and #83.
Type of Change
Checklist
devdocker compose uporuvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.How to Test
"status":"healthy".Visual / UI changes - REQUIRED if you touched anything that renders
No visual/UI rendering changes intended. This PR does not touch
static/js, CSS, HTML, icons, layout, or rendered component styling.--red,--fg,--bg,--card,--border, etc.) - do not introduce new color values, font sizes, or spacing units.static/index.html) or plain text.Fira Code) for primary UI text. Don't override.Screenshots / clips
Not applicable; no visual/UI rendering changes intended.