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:
Originallowlist on mutating requests (POST / PUT / DELETE / PATCH) to/api/*. Requests with noOriginor a foreignOriginare refused with403 origin_not_allowed.- CSRF token required on mutating
/api/*. A per-process random token is served byGET /api/csrfto same-origin clients only; mutating requests must echo it inX-CSRF-Token. Compared withsecrets.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) onast.literal_evalof 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:
stateparameter 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
resourceindicator 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 (alias ↔ real_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.