Skip to content

Security: tunnel exposes /api/* without token authentication #77

Description

@kovtcharov

Summary

When Claudia is exposed over a tunnel (localtunnel/ngrok), the tunnel auth middleware validates the access token for the root document but lets all /api/* requests through with no token check. Anyone who learns the public tunnel URL can call the REST API unauthenticated. This is pre-existing (shipped since v0.2.30) and was surfaced during the deep review of #75.

Evidence

backend/src/server.ts (tunnel middleware):

  • L408–412 — the / route requires and validates ?token= via tunnelManager.validateToken.
  • L420–421 — if (req.path.startsWith('/api/')) return next(); → every API route skips the token check over the tunnel.
  • L1234–1238 — the WebSocket path does validate the token. So REST and WS are inconsistent.

frontend/src/config/api-config.ts:

  • getWebSocketUrl() appends ?token= for tunnel access (L58–61), but getApiBaseUrl() returns bare window.location.origin (L38–40) — REST calls carry no token today.

Impact

Why it's not a one-line fix

Adding a server-side token check for /api/* would break the existing frontend/mobile clients, which don't send the token on REST requests. A correct fix is cross-cutting:

  1. Backend: require + validate the tunnel token for /api/* (and /api/voice/*) when isTunnelHost(host), accepting it via an Authorization/X-Tunnel-Token header (preferred over a query param to avoid token leakage in logs/referrers).
  2. Frontend: a centralized authed-fetch wrapper that attaches the token to every API request over the tunnel (today fetch calls are scattered and use getApiBaseUrl() directly).
  3. Test end-to-end through an actual tunnel + the mobile flow.

Suggested follow-ups

  • Prefer a header-based token over ?token= to reduce leakage; consider short-lived/rotating tokens.
  • For GET /api/voice/deepgram-token, return an ephemeral scoped key (Deepgram Management API) rather than the raw long-lived key.
  • Consider basic rate limiting on tunnel-exposed routes.

Severity: high (network-exposed, unauthenticated). Labeling for triage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions