Hi — while evaluating Voicebox for a local integration I ran a security pass over the server and the Tauri shell and found two issues worth hardening. I have fixes applied locally against b35b909 and I'm happy to open a PR.
1. (High) No Host-header / DNS-rebinding protection on the local server
backend/app.py (create_app()) and the mounted MCP ASGI sub-app (backend/mcp_server/server.py) listen on 127.0.0.1:17493 but don't validate the Host header. The unauthenticated REST endpoints (/speak, /transcribe, /shutdown) and the MCP transport are therefore reachable via DNS rebinding: a malicious page the user visits can rebind its own hostname to 127.0.0.1 and drive the server from the browser — speak, enumerate/read transcripts, shut it down, and (via audio_path) read local files — because the request is loopback-sourced and nothing checks Host.
Fix — add Starlette's TrustedHostMiddleware with a loopback allow-list, on both the outer FastAPI app and the FastMCP sub-app (http_app(...) accepts a middleware= list):
from starlette.middleware import Middleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
ALLOWED = ["127.0.0.1", "localhost", "127.0.0.1:17493", "localhost:17493"]
application.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED)
mcp_app = mcp.http_app(
path="https://github.com/", transport="http",
middleware=[Middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED)],
)
After this, Host: evil.com returns 400 while Host: 127.0.0.1:17493 is served normally (I verified this with an in-process Starlette TestClient).
(Optional defense-in-depth: gate the audio_path file read on a per-session token rather than a bare loopback check. With the Host gate in place this is no longer the primary exposure — it closes the residual case of a non-browser client that can set an arbitrary Host.)
2. (High) Over-broad Tauri capabilities
tauri/src-tauri/capabilities/default.json + tauri.conf.json grant the webview more than the app's own Rust invokes:
shell:allow-execute + shell:allow-spawn — arbitrary process execution, not used by the app
fs:read-all + fs:write-all — whole-filesystem access
shell.open scope ".*" — open any URL/scheme
"csp": null — no Content-Security-Policy
remote.urls: ["http://localhost:*"] — any localhost port
A compromised or hostile web context in the webview could leverage these.
Recommended fix — drop shell:allow-execute/shell:allow-spawn; narrow shell.open to only the x-apple.systempreferences: schemes the app actually opens (avoid a catch-all or a broad absolute-path scope — a permissive shell.open is itself a vector); replace fs:read-all/fs:write-all with scoped fs permissions for the app-data dir (keep a scoped read if the app loads voice/audio files at runtime); set a real CSP (default-src 'self'; connect-src http://127.0.0.1:17493 …); and pin remote.urls to the specific 127.0.0.1:17493 origin (+ the Vite dev port).
Note: I applied these locally but haven't exercised a full signed Tauri build (the build pulls a prebuilt voicebox-server sidecar I don't have), so the capability changes are static-reviewed — worth a build-test on your side before merging.
Offer
Happy to open a PR with the changes, or you're welcome to take them directly — whichever is easier for you.
Hi — while evaluating Voicebox for a local integration I ran a security pass over the server and the Tauri shell and found two issues worth hardening. I have fixes applied locally against
b35b909and I'm happy to open a PR.1. (High) No Host-header / DNS-rebinding protection on the local server
backend/app.py(create_app()) and the mounted MCP ASGI sub-app (backend/mcp_server/server.py) listen on127.0.0.1:17493but don't validate theHostheader. The unauthenticated REST endpoints (/speak,/transcribe,/shutdown) and the MCP transport are therefore reachable via DNS rebinding: a malicious page the user visits can rebind its own hostname to127.0.0.1and drive the server from the browser — speak, enumerate/read transcripts, shut it down, and (viaaudio_path) read local files — because the request is loopback-sourced and nothing checksHost.Fix — add Starlette's
TrustedHostMiddlewarewith a loopback allow-list, on both the outer FastAPI app and the FastMCP sub-app (http_app(...)accepts amiddleware=list):After this,
Host: evil.comreturns400whileHost: 127.0.0.1:17493is served normally (I verified this with an in-process StarletteTestClient).(Optional defense-in-depth: gate the
audio_pathfile read on a per-session token rather than a bare loopback check. With the Host gate in place this is no longer the primary exposure — it closes the residual case of a non-browser client that can set an arbitraryHost.)2. (High) Over-broad Tauri capabilities
tauri/src-tauri/capabilities/default.json+tauri.conf.jsongrant the webview more than the app's own Rust invokes:shell:allow-execute+shell:allow-spawn— arbitrary process execution, not used by the appfs:read-all+fs:write-all— whole-filesystem accessshell.openscope".*"— open any URL/scheme"csp": null— no Content-Security-Policyremote.urls: ["http://localhost:*"]— any localhost portA compromised or hostile web context in the webview could leverage these.
Recommended fix — drop
shell:allow-execute/shell:allow-spawn; narrowshell.opento only thex-apple.systempreferences:schemes the app actually opens (avoid a catch-all or a broad absolute-path scope — a permissiveshell.openis itself a vector); replacefs:read-all/fs:write-allwith scoped fs permissions for the app-data dir (keep a scoped read if the app loads voice/audio files at runtime); set a real CSP (default-src 'self'; connect-src http://127.0.0.1:17493 …); and pinremote.urlsto the specific127.0.0.1:17493origin (+ the Vite dev port).Note: I applied these locally but haven't exercised a full signed Tauri build (the build pulls a prebuilt
voicebox-serversidecar I don't have), so the capability changes are static-reviewed — worth a build-test on your side before merging.Offer
Happy to open a PR with the changes, or you're welcome to take them directly — whichever is easier for you.