-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
git-doc-mcp executes untrusted code from remote URLs. Its security model uses defense-in-depth with three isolation layers, strict URL validation, and scoped secret access.
┌───────────────────────────────────────┐
│ Main Process (CLI) │
│ - Loads manifest │
│ - Manages secrets │
│ - MCP protocol │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Worker Process (Layer 1) │ │
│ │ - Separate Node.js process │ │
│ │ - Sanitized environment │ │
│ │ - Crash isolation │ │
│ │ │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ isolated-vm (Layer 2) │ │ │
│ │ │ - V8 isolate │ │ │
│ │ │ - Memory: 128MB │ │ │
│ │ │ - CPU timeout: 30s │ │ │
│ │ │ - No filesystem access │ │ │
│ │ │ - No network access │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ ctx API (Layer 3) │ │ │ │
│ │ │ │ - URL validation │ │ │ │
│ │ │ │ - Secret scoping │ │ │ │
│ │ │ │ - Audit logging │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└───────────────────────────────────────┘
- Separate Node.js process with sanitized environment variables
- Crash isolation: worker crash doesn't affect the main process
- Communication via newline-delimited JSON over stdin/stdout
- V8 isolate: completely separate JavaScript heap
-
Memory limit: 128MB (configurable via
--memory-limit, range 8MB-1GB) - CPU timeout: 30 seconds (hardcoded)
- No direct access to filesystem, network, or Node.js APIs
- Action code can only interact with the outside world through
ctxcallbacks
-
ctx.fetch: All URLs validated for SSRF before any request -
ctx.getSecret: Secrets only returned when URL matches the scope pattern -
ctx.log: Safe logging (strings only, no code injection) - Audit logging: Every fetch and secret access is logged
All HTTP(S) URLs are validated before any network request, whether from the manifest (action URLs, resource URIs) or from action scripts (ctx.fetch).
Local file paths for action and resource.uri fields bypass SSRF validation -- they are read directly from disk, not over the network. Local file paths are only allowed when the manifest itself is loaded from a local file; remote manifests with local paths are rejected.
| Category | Examples |
|---|---|
| Private IPv4 |
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
| Loopback |
127.0.0.0/8, localhost
|
| Link-local | 169.254.0.0/16 |
| Private IPv6 |
::1, fc00::/7, fe80::/10
|
| HTTP (default) | All http:// URLs (use --allow-http to override) |
URLs are resolved to IP addresses and validated:
- DNS lookup is performed
- Resolved IPs are checked against blocked ranges
- If any resolved IP is private, the request is blocked
- This prevents DNS rebinding attacks
Every redirect is re-validated:
- Redirect URL is resolved and checked for SSRF
- Cross-origin redirects strip
AuthorizationandCookieheaders - Maximum 5 redirects (not configurable)
- Relative redirects are resolved against the current URL
First run:
manifest.yml → SHA256 → stored in ~/.git-doc-mcp/
Subsequent runs:
manifest.yml → SHA256 → compare with stored hash
├── Match: OK, proceed
├── Mismatch + --trust-changed: accept new hash
└── Mismatch (default): FAIL, refuse to start
For CI/CD or security-critical deployments, pin the manifest hash explicitly:
git-doc-mcp --manifest https://example.com/manifest.yml \
--manifest-hash "sha256:a1b2c3d4e5f6..."Every action script has a SHA256 hash in the manifest:
tools:
- name: my-tool
action: https://example.com/actions/my-tool.v1.js
actionHash: "sha256:a1b2c3d4e5f6..."Before execution, the fetched action code is hashed and compared. If the hash doesn't match, execution is refused.
Secrets are never freely accessible to action scripts. Every ctx.getSecret(name, url) call validates the URL against the secret's declared scope.
secrets:
- name: GITHUB_TOKEN
scope:
- "https://api.github.com/*"
- "https://raw.githubusercontent.com/*"// In action script:
ctx.getSecret('GITHUB_TOKEN', 'https://api.github.com/repos/owner/repo');
// → Returns the token (URL matches scope)
ctx.getSecret('GITHUB_TOKEN', 'https://evil.com/steal');
// → Returns undefined (URL doesn't match scope)-
Exact origin:
api.github.comwon't matchevil.api.github.com -
Path-boundary wildcards:
/repos/*matches/repos/foobut not/repos-private - No scheme downgrade: HTTPS scope won't match HTTP URLs
- Audit logging: All access attempts (granted and denied) are logged
Sliding-window rate limiter to prevent abuse:
# Default: 60 calls per minute
git-doc-mcp --manifest ./manifest.yml --rate-limit 60
# Unlimited (for trusted environments)
git-doc-mcp --manifest ./manifest.yml --rate-limit 0Rate limiting applies to tool calls, not individual fetch requests within actions.
All security-relevant events are logged to ~/.git-doc-mcp/logs/audit.jsonl:
{"timestamp":"2024-01-15T10:30:00Z","event":"fetch","url":"https://api.github.com/repos/...","status":"200","duration_ms":150,"manifestName":"my-mcp"}
{"timestamp":"2024-01-15T10:30:01Z","event":"secret-access","secretName":"GITHUB_TOKEN","url":"https://api.github.com/...","status":"allowed","manifestName":"my-mcp"}
{"timestamp":"2024-01-15T10:30:02Z","event":"redirect","url":"https://new.example.com/...","details":{"from":"https://old.example.com/..."},"manifestName":"my-mcp"}Logs are automatically rotated to prevent disk exhaustion.
- Use HTTPS for all remote action URLs and resource URIs (local file paths are acceptable for development)
- Compute accurate SHA256 hashes for all action scripts
- Scope secrets to the minimum required URL patterns
- Mark secrets as
required: falseunless the tool truly can't function without them - Validate API responses in action scripts before using data
- Use specific error messages that don't leak internal details
- Version action scripts (e.g.,
my-tool.v2.js) rather than modifying in place - Test with
--manifest-hashpinning before deploying to production