Commit Graph
12 Commits
Author SHA1 Message Date
Paolo Antinori 871d68a6ff fix(auth): offer API-key login on server-mode admin 403s (#1569)
Fix server-mode admin authentication recovery without trapping PIN-only deployments, and prevent stale 403 responses from clearing or superseding newly issued sessions. Includes backend/frontend regression coverage, docs, and changelog credit for @paoloantinori.
2026-08-20 00:03:37 +00:00
19ae20111a fix(security): replace persistent admin keys with scoped sessions (#1528)
* fix(security): replace persistent admin keys with sessions

Exchange the remote administrator key once for bounded, revocable credentials. Canonicalize backend principals, enforce cookie CSRF and exact origins, and use path-bound one-use WebSocket tickets.

Migrate the bundled UI away from durable master-key storage and credential-bearing URLs. Add unit, integration, static-hygiene, and production-browser regressions plus synchronized operator documentation.

* docs: link session hardening to PR 1528

* fix(security): key session indexes with process pepper

Use HMAC-SHA-256 instead of an unkeyed digest for in-memory session and WebSocket-ticket indexes. This preserves constant-size lookup identifiers, makes copied records unusable without the process pepper, and resolves CodeQL's weak sensitive-data hash finding.

* fix(auth): align empty bearer migration precedence

Centralize the Authorization-channel presence decision with canonical principal parsing. Bearer followed only by spaces now remains an empty channel during legacy cookie migration, while unsupported or invalid explicit credentials stay authoritative and fail closed.

* fix(security): harden admin session review boundaries

* fix(security): derive key generations with HKDF

* fix(auth): anchor the admin-session store so module reloads cannot fork it

test_master_exchange_does_not_bypass_pin_on_normal_routes failed in full-suite
runs: test_mcp_bindings' client fixture purges the services.* tree from
sys.modules and reloads main, so api.routers.auth re-imported a fresh
services.admin_sessions (new AdminSessionStore) while core.auth kept its
import-time reference to the old one — the exchange issued the cookie into
one store and the middleware resolved it against another, turning the
expected "PIN required" into "API key required".

Root cause is the class of bug, not the one test: a process-global auth
store defined as a bare module-level singleton forks under importlib.reload
or purge-and-reimport. Fix at the source: admin_session_store now resolves
through a synthetic sys.modules anchor (_omnivoice_admin_session_store_anchor)
that reloads never re-execute and package-prefix purges never match, so every
copy of the module shares the one per-process store. No consumer or behavior
changes.

Regression test reproduces both fork vectors (in-place reload and
sys.modules purge + fresh import) and asserts previously issued sessions
still resolve and the store identity is preserved; it fails before this fix
and passes after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): honor X-Forwarded-Proto for CSRF origin and Secure cookies behind TLS proxies

Behind Tailscale Serve (docs/remote-gpu.md) or any TLS-terminating proxy,
the browser talks https while the backend hop stays http, so exact-origin
CSRF compared an https Origin against an http expectation and rejected
every legitimate request, and the session cookie shipped without Secure.
uvicorn's ProxyHeadersMiddleware only rewrites the scope for loopback
peers, which misses Docker and any non-loopback proxy topology.

New core.csrf.effective_scheme derives the client-facing scheme: resolved
scope first (uvicorn's trusted-proxy rewrite wins), then an upgrade-only
read of X-Forwarded-Proto's first value — https/wss promotes http to
https, everything else is ignored, and a genuine TLS hop can never be
downgraded. Used by both the destination-origin comparison and
auth._secure_cookie so the WS-ticket/logout CSRF paths and the cookie
Secure flag agree. Spoofing gains nothing: the host:port half of the
origin tuple is untouched, browsers cannot attach the header cross-site
without a preflight this API never grants, and forging it on plain http
only adds Secure (the browser then drops the cookie — self-harm only).

Regression tests: proxied https origin accepted (origin check, Secure
flag, logout), comma-separated chains, scope-fallback path, spoofed
header still rejects cross-origin, cannot downgrade real https, junk
values ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): consume the stored admin key only after a successful exchange

A remote-backend user upgrading with their backend unreachable lost the
only stored copy of OMNIVOICE_API_KEY: every migration path deleted the
durable ov_api_key BEFORE the session exchange settled, stranding them
until they recovered the key from the server box. Close the whole class:

- client.ts bootstrap: read the legacy key, exchange first, and remove
  the durable copy only after the exchange succeeds; on failure the key
  stays so the next launch retries the migration (auth gate still rises).
- authSession.ts exchangeApiKey: move removeLegacyMaster from before the
  fetch to the cookie/bearer success paths — the key never coexists with
  a live session, but a rejected or hung exchange no longer consumes it.
- remoteBackendProbe.ts configuredRemoteBackend: stop wiping the key on
  every app mount.
- RemoteBackendPanel: a connection test or an aborted save no longer
  wipes the pending key; only disabling the remote backend discards it.
- prefKeys.js: ov_api_key moves from PREF_KEYS to PRESERVED_KEYS —
  factory reset preserves the pending connection credential exactly like
  ov_backend_url; the successful migration is what deletes it.

Tighten the credential-hygiene static guard to match: it accepted
sessionStorage.setItem('ov_api_key', …) — the exact class it exists to
close. The guard now flags .setItem(<master key>) on any storage
receiver, quote style, or injected-store alias, with a self-test pinning
what it catches and what stays legal.

Fail-before/pass-after regression tests: backend unreachable retains the
key and the next bootstrap retries it; a successful exchange removes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(auth): make session validation occupancy-independent

* test(auth): catch optional master-key storage calls

* feat(docs): add PR control document for bultodepapas in VoiceStudio

* docs: keep the PR tracking board in the fork; credit the changelog line

The pr-control document is excellent process discipline, but it is the
contributor's own operational board (their inventory, their update
commands) — it lives naturally in their fork, and docs/agents/ here is
context every repo agent loads. Removed with appreciation; the changelog
line gains its contributor credit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: debpalash <4178343+debpalash@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:46:24 +00:00
Giuseppe Rojas cd54113173 fix(security): close server-mode admin bypasses (#1525)
* fix(security): require keys for remote admin actions

* fix(frontend): guard unavailable scrollIntoView

* docs: link changelog to PR 1525

* fix(security): align PIN-only discovery policy

* fix(security): preserve strict sidecar boundary

* fix(security): normalize remote API keys

* fix(auth): normalize credential fallback order
2026-08-13 05:04:29 +00:00
debpalash c2b9951f28 Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary
# Conflicts:
#	CHANGELOG.md
#	backend/core/path_authorization.py
#	frontend/src-tauri/src/commands.rs
#	tests/test_network_share.py
2026-08-10 04:32:23 +00:00
Palash Debnath ae46f187d0 fix(security): move host-path authority into the desktop shell (#1448)
Close unauthenticated remote mutation paths and keep filesystem destinations behind one-shot Tauri capabilities. CodeRabbit and Greptile findings were fixed on-branch; all review threads are resolved. Full CI, Security, Rust, and cross-platform smoke checks are green.
2026-08-09 21:55:43 +00:00
debpalash fa3ba2f366 fix(security): authorize host paths through native IPC 2026-08-09 21:23:00 +00:00
debpalash 7abc07e7f3 fix(security): keep host paths desktop-only 2026-08-09 21:10:51 +00:00
debpalash f8eeb5a963 fix(security): require API key for remote admin writes 2026-08-09 21:01:14 +00:00
debpalash 204eff2be1 fix(security): #1213 review — the share PIN must not gate RCE-class admin
CodeRabbit: the 6-digit share PIN is brute-forceable (10^6, no lockout), so
letting it unlock admin over the network was still weak. Admin now requires the
API key (a long operator secret) or loopback; the PIN is consumption-only and
never gates /system/* or /api/settings/*. A PIN-only deployment keeps admin
loopback-only. Docs (api-auth.md, remote-gpu.md) aligned with the conditional
gate (no credential -> admin open; API key -> admin) and the PIN exclusion.
Test inverted: presenting the PIN over the network is now 403 on admin.
2026-07-20 22:57:31 +05:30
debpalashandClaude Opus 4.8 3b879f298e fix(auth): keep admin gate independent of trusted-network trust under server mode (#1213)
OMNIVOICE_SERVER_MODE=1 made require_loopback an unconditional no-op, so with
OMNIVOICE_TRUSTED_NETWORKS also set a trusted-CIDR client — a consumption-only
exemption that bypasses the PIN/API-key middleware via is_local_host — could
reach the RCE-class admin surface (/system/set-env, /api/settings/*) with no
credential. That collapsed the documented two-tier privilege model
(consumption trust != admin trust) in exactly the "lock the backend with a key,
exempt a LAN proxy for TTS" configuration.

Server mode still can't require true loopback (Docker NAT, #261), but it now
applies the credential rule to admin routes: open only when NO credential is
configured; otherwise the request must present the API key or share PIN.
Trusted-network membership alone never satisfies it. Loopback, credential
holders, and no-credential Docker deployments are unchanged; consumption
routes (require_local / middleware) keep exempting trusted networks.

Regression tests cover the server-mode x trusted-network x credential matrix
(fail-before/pass-after). Docs: new docs/api-auth.md two-tier model + quick
reference; docs/remote-gpu.md corrected (previously documented the hole as
accepted behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:22:08 +05:30
Paolo Antinori f669687ed1 feat(backend): trust a local network/proxy via OMNIVOICE_TRUSTED_NETWORKS (#1170)
Self-hosting behind a reverse proxy or on a LAN used to force a blunt choice:
OMNIVOICE_SERVER_MODE (trust every non-loopback source) or the API-key/PIN gates
— which a proxy that strips the Authorization header breaks for browser clients
entirely.

Add OMNIVOICE_TRUSTED_NETWORKS (comma-separated CIDRs) whose addresses are
treated as trusted by the CONSUMPTION gates (PIN/API-key middleware, dictation
WebSocket) via is_local_host — a LAN/proxy client is exempted from consumption
auth. Admin routes (require_loopback → /system/set-env, /api/settings/*) stay
true-loopback-only (is_loopback, not is_local_host) to preserve the two-tier
privilege model: consumption trust ≠ admin trust (RCE-class surface). Opt-in,
default empty → zero behavior change. The granular companion to server-mode (#261).

Tests: is_loopback / is_local_host / require_loopback contract for trusted CIDRs,
adjacent subnets, malformed entries, the two-tier split (trusted-network rejected
by the admin gate), and the default (no-trust) case. Docs + CHANGELOG.
2026-07-17 17:33:18 +02:00
Palash DebnathandClaude Opus 4.8 b4f1fe18d7 fix(server): relax loopback gate in headless server mode so Docker admin UI works (#261) (#263)
In Docker the loopback origin gate (`require_loopback`) is unenforceable:
Docker's NAT rewrites `request.client.host` to the bridge gateway (e.g.
172.17.0.1) even for a localhost-only `-p 127.0.0.1:3900:3900` mapping, so every
request looks non-loopback. The gate then 403s the operator out of the routes
the web UI needs — `/system/*` (incl. `/system/info`, which left the version
blank, re-breaking #249 in Docker) and `/api/settings/*` (HF-token entry) —
surfacing as "Loopback origin required" all over the UI.

Fix: add an explicit, opt-in `OMNIVOICE_SERVER_MODE` flag. When set,
`require_loopback` becomes a no-op; exposure is then governed by the operator's
port mapping plus the optional share PIN (NetworkAccessMiddleware still 401s
unauthenticated non-loopback clients whenever a PIN is set). The Docker image
sets `OMNIVOICE_SERVER_MODE=1` (Dockerfile + documented in compose).

Security: the desktop build NEVER sets this, so its loopback boundary is
unchanged — LAN share guests are still denied the admin/system routes. New
unit tests lock the contract (strict 403 by default incl. the PR #81 vectors;
relaxed only under the flag). Existing non-loopback 403 tests still pass.

Docs: docker.md troubleshooting entry for "Loopback origin required".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:22:25 +05:30