Skip to content

Commit 43f6f74

Browse files
aborrusoclaude
andcommitted
docs: document mandatory HTTP allowlist (CKAN_ALLOWED_DOMAINS / CKAN_HTTP_ALLOW_ALL)
Add a Security section to README and update CLAUDE.md: the HTTP transport now requires CKAN_ALLOWED_DOMAINS (default-deny) and refuses to start without it unless CKAN_HTTP_ALLOW_ALL=true. Fix the TRANSPORT=http examples that would otherwise fail to start (v0.4.108). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bb7439b commit 43f6f74

2 files changed

Lines changed: 26 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ npm run test:coverage
5454
npm start
5555

5656
# Start server in HTTP mode (for remote access)
57-
TRANSPORT=http PORT=3000 npm start
57+
# HTTP requires a domain allowlist (or CKAN_HTTP_ALLOW_ALL=true to opt out)
58+
CKAN_ALLOWED_DOMAINS="www.dati.gov.it" TRANSPORT=http PORT=3000 npm start
5859

5960
# Watch mode for development
6061
npm run watch
@@ -152,7 +153,7 @@ When making changes:
152153
1. Build locally: `npm run build`
153154
2. Run automated tests: `npm test`
154155
3. Run real HTTP server tests to verify end-to-end behavior:
155-
- Start: `TRANSPORT=http PORT=3001 node dist/index.js & disown`
156+
- Start: `CKAN_ALLOWED_DOMAINS="www.dati.gov.it,dati.comune.messina.it" TRANSPORT=http PORT=3001 node dist/index.js & disown`
156157
- Call each affected tool via curl against a real CKAN portal
157158
- Stop: `kill $(lsof -ti:3001)`
158159

@@ -362,7 +363,7 @@ For manual testing, use HTTP transport with curl:
362363
```bash
363364
# Terminal 1 — start server
364365
npm run build
365-
TRANSPORT=http PORT=3001 node dist/index.js
366+
CKAN_ALLOWED_DOMAINS="www.dati.gov.it" TRANSPORT=http PORT=3001 node dist/index.js
366367
```
367368

368369
```bash
@@ -424,7 +425,8 @@ To test with Claude Desktop, add MCP configuration to config file.
424425
- **Caching**: Read-through cache in `makeCkanRequest`. Action-based TTL (metadata 300s, datastore 60s, status 3600s). Backend: Cloudflare Cache API on Workers, in-memory LRU on Node. Disable with `CKAN_CACHE_ENABLED=false`. Env vars: `CKAN_CACHE_TTL_DEFAULT`, `CKAN_CACHE_MAX_ENTRIES`, `CKAN_CACHE_MAX_ENTRY_BYTES`.
425426
- **No authentication**: Uses only public CKAN endpoints
426427
- **No WebSocket**: MCP over HTTP uses JSON responses (not SSE streaming in Workers)
427-
- **Domain allowlist**: Optional SSRF hardening via `CKAN_ALLOWED_DOMAINS=domain1.com,domain2.org`. If set, requests to unlisted domains are blocked. Default: no restriction (all public domains allowed). Enforced in `validateServerUrl()`.
428+
- **SSRF protection** (v0.4.108+): requests are validated against private/internal IP ranges, including hostnames that *resolve* to internal addresses (DNS-based SSRF) — `validateServerUrl()` (string/literal guard) + `isBlockedIp()` + a connection-pinning `lookup` agent on the Node/axios path and `assertHostnameResolvesSafe()` on the fetch/SPARQL path.
429+
- **Domain allowlist**: `CKAN_ALLOWED_DOMAINS=domain1.com,domain2.org` (comma-separated, default-deny when set). **Mandatory for the HTTP transport**: `TRANSPORT=http` refuses to start without it, unless `CKAN_HTTP_ALLOW_ALL=true` is set (logs a warning). `stdio` is unaffected (no allowlist required). Enforced in `validateServerUrl()` + `assertHttpAllowlistConfigured()`.
428430
- **Audit logging**: Every `makeCkanRequest` call writes a JSON line to stderr (Node modes only; Workers use `console.log`). Fields: `ts`, `server`, `action`, `cache_hit`, plus relevant query params (`q`, `fq`, `sql` truncated to 200 chars, `id`, `rows`, `limit`).
429431

430432
### Adding New Tools

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -828,11 +828,29 @@ npx @modelcontextprotocol/inspector node dist/index.js
828828

829829
Opens at `http://localhost:5173`.
830830

831+
### Security: HTTP transport requires a domain allowlist
832+
833+
The HTTP transport (`TRANSPORT=http`) is network-exposed and **unauthenticated**: any
834+
client that reaches `POST /mcp` can drive requests through it. To prevent SSRF abuse
835+
(e.g. a caller pointing `server_url` at internal hosts or cloud metadata), the HTTP
836+
transport **refuses to start** unless you set a domain allowlist:
837+
838+
| Variable | Effect |
839+
|---|---|
840+
| `CKAN_ALLOWED_DOMAINS` | Comma-separated allowlist of hostnames the server may query (default-deny). **Required** to start the HTTP transport. Example: `CKAN_ALLOWED_DOMAINS="www.dati.gov.it,dati.comune.messina.it"` |
841+
| `CKAN_HTTP_ALLOW_ALL=true` | Explicit opt-out: start the HTTP transport **without** an allowlist (logs a security warning). Not recommended when network-exposed. |
842+
843+
The default `stdio` transport is unaffected — it stays open so you can query any portal
844+
locally. Regardless of allowlist, all requests are also validated against private/internal
845+
IP ranges, including hostnames that *resolve* to internal addresses (DNS-based SSRF, fixed
846+
in v0.4.108). The official Cloudflare Worker is sandboxed by the platform and does not
847+
require this setting.
848+
831849
### Manual HTTP Testing
832850

833851
```bash
834-
# Start server
835-
TRANSPORT=http PORT=3001 node dist/index.js
852+
# Start server (HTTP needs an allowlist — see "Security" above)
853+
CKAN_ALLOWED_DOMAINS="www.dati.gov.it" TRANSPORT=http PORT=3001 node dist/index.js
836854

837855
# List available tools
838856
curl -s -X POST http://localhost:3001/mcp \

0 commit comments

Comments
 (0)