Skip to content

Security model

This page describes what OpenMaskit treats as a real threat and what it doesn't, so you can evaluate whether it fits your environment.

Deployment model

OpenMaskit runs locally on the user's own machine — like a CLI dev tool (docker, psql, jupyter, a local DB client). It is not a hosted service. The Python proxy is not deployed anywhere; the dashboard and the MCP endpoint both bind to 127.0.0.1 by default.

That deployment model is what motivates the in-scope / out-of-scope split below.

In scope

Browser-based cross-origin attacks against localhost

The dashboard runs on 127.0.0.1:9473. A webpage the user happens to visit in another tab could try to fetch() against it and exfiltrate secrets or trigger destructive actions. This is the canonical "localhost service" attack class (cf. Docker daemon CVEs, ethdev wallets, etc.).

Defenses in 0.6.0:

  • Origin allowlist on mutating requests (POST / PUT / DELETE / PATCH) to /api/*. Requests with no Origin or a foreign Origin are refused with 403 origin_not_allowed.
  • CSRF token required on mutating /api/*. A per-process random token is served by GET /api/csrf to same-origin clients only; mutating requests must echo it in X-CSRF-Token. Compared with secrets.compare_digest.
  • Body size cap (OPENMASKIT_MAX_REQUEST_BYTES, default 1 MiB). Bounds the damage from a request that wedges parsing.

The MCP endpoint on :9474 is unaffected — real MCP clients send neither Origin nor CSRF and need to keep working.

Malicious upstream MCP server

OpenMaskit talks to third-party MCP servers. Their responses must not be able to crash the proxy, exhaust memory, or poison persistent state.

Defenses:

  • Per-text-block parse cap (OPENMASKIT_MAX_PARSE_BYTES, default 1 MiB) on ast.literal_eval of upstream tool responses. A giant nested literal would otherwise OOM the proxy.
  • Field stripping lets you remove fields entirely so even an alias is not created.
  • Hidden tools let you remove dangerous tools from the agent's view.
  • Guardrails let you refuse to forward calls whose arguments match dangerous patterns.

OAuth flow integrity

OAuth flows physically go through the browser, so the usual web defenses apply:

  • state parameter validation on every callback, with a 15-minute TTL.
  • PKCE (RFC 7636 S256) on every authorization request; the verifier is stashed server-side and never leaves the local process.
  • RFC 8707 resource indicator binds the issued access token to the specific upstream MCP server.
  • Per-server token files (~/.openmaskit/oauth/{handle}.json), Fernet-encrypted at rest.
  • The OAuth callback path (/oauth/callback/{handle} on the dashboard port) only accepts callbacks for known in-flight flows.
  • Dynamic Client Registration implements RFC 8414 path-aware discovery, RFC 9728 resource metadata, RFC 7591 dynamic registration, and RFC 8252 §7.3 native-app loopback.

Out of scope

The threats below are out of scope for the local CLI deployment model. If you wrap OpenMaskit in something else (a shared dev VM, a hosted environment), they may become relevant again.

  • Multi-user shared-machine attacks. Another local user on the same machine reading ~/.openmaskit/.key, traffic.db, store.db, or OAuth token files is not defended against beyond standard file permissions.
  • Network attackers between the dashboard and the browser. Everything is localhost; there is no TLS by design.
  • Side-channel attacks against the masking engine's per-target counters.

Encryption at rest

Data Storage Protection
OAuth tokens ~/.openmaskit/oauth/{server_id}.json Fernet (AES-128-CBC + HMAC-SHA256)
Server configs (env, headers, secrets) store.db mcp_servers.config_enc Fernet
Audit log unmasked args + response traffic.db args_enc, response_enc Fernet
Aliases (aliasreal_value) store.db mappings Plaintext (kept local; alias map is the masking engine's working set)
Masking rules, guardrails, etc. store.db Plaintext (config, not secret)

The encryption key is ~/.openmaskit/.key by default. Override with OPENMASKIT_ENCRYPTION_KEY if you want to source it from a real secret manager.

Network exposure

Endpoint Port Bound to Purpose
Dashboard 9473 127.0.0.1 Web UI + JSON API + OAuth callback
MCP HTTP endpoint 9474 127.0.0.1 Where AI agents connect

To bind to a non-loopback address (e.g. inside a Docker container so the host can reach it), set OPENMASKIT_HOST=0.0.0.0. The shipped Docker image does this.

Binding to 0.0.0.0

Binding to 0.0.0.0 exposes both ports on every network interface, including any LAN, VPN, or Tailscale interface. CSRF and Origin checks still apply, but you become responsible for keeping the dashboard off untrusted networks.

What OpenMaskit sends to *.maskitmcp.com

The hosted marketplace backend sees:

  • api.maskitmcp.com — anonymous installation ID + version on catalog browse / detail / version-check calls. No tool data, no server configs, no secrets.

Set OPENMASKIT_DISABLE_MARKETPLACE=1 to opt out of all backend calls. Custom servers added via the dashboard continue to work without any backend contact.

Reporting a vulnerability

See SECURITY.md in the repository for the disclosure process.