Skip to content

Security hardening: DNS-rebinding (Host-header) exposure on the local API/MCP server + over-broad Tauri capabilities — fixes available #778

Description

@fsebens

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions