@aborruso/ckan-mcp-server 0.4.108 → 0.4.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LOG.md +61 -0
- package/dist/index.js +246 -112
- package/dist/worker.js +118 -116
- package/package.json +1 -1
package/LOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# LOG
|
|
2
2
|
|
|
3
|
+
## 2026-07-31
|
|
4
|
+
|
|
5
|
+
### v0.4.113
|
|
6
|
+
|
|
7
|
+
JSON output always parseable (issue #39).
|
|
8
|
+
|
|
9
|
+
The docs promised "always valid JSON"; the code did not deliver it. Surfaced while reviewing PR #38 (an AI contribution by `averyquinnhq`), which documented the real behaviour and contradicted issue #37 — the PR was right.
|
|
10
|
+
|
|
11
|
+
- **Live repro**: `ckan_tag_list limit=1000` on dati.gov.it → 50,046 characters, `JSON.parse` fails. Not a corner case: an ordinary call.
|
|
12
|
+
- **`truncateJson`**: the last-resort branch cut the serialized string (`truncateText(JSON.stringify(...))`, with an in-code comment admitting "may produce invalid JSON"). It now empties arrays progressively and, when that is not enough, degrades to a valid `{_truncated, _error}`. `SHRINKABLE_KEYS` extended (+`rows`, `datasets`, `portals`, `facets`, `fields`) with a sacrifice order: bulk rows first, `fields` last.
|
|
13
|
+
- **8 tools + 4 Resources** used `truncateText(JSON.stringify(...))` directly: routed through `truncateJson`. `sparql_query` appended `/* output truncated */` to cut JSON (JSON has no comments): rewritten.
|
|
14
|
+
- **4 tools with no cap at all**: `ckan_get_mqa_quality`, `ckan_get_mqa_quality_details` (bare JSON.stringify), `ckan_find_portals`, `ckan_status_show` (markdown).
|
|
15
|
+
- **A third unparseable path**: no `catch` honoured `response_format` — every error came back as prose even with `json`. New `formatError()`; errors now return `{error, _error: true}` plus `isError: true`. Zod validation errors stay textual (emitted by the SDK before the handler runs).
|
|
16
|
+
- **`structuredContent` stays uncapped** (65,382 characters against 50,000 of text on tag_list): capping it would drop rows from `datastore-table-ui`, which consumes it. Decision deferred, documented in `docs/DECISIONS.md`.
|
|
17
|
+
- 10 new tests (`truncateJson` had none), 453 passing. E2e: `tag_list` from PARSE-FAIL to parse-ok; real errors parseable in json mode.
|
|
18
|
+
- `CONTRIBUTING.md`: section on AI-assisted contributions (disclosure, small diffs, verifiable claims).
|
|
19
|
+
- Review follow-up: `addDemoFooter()` was appended to JSON output in `quality.ts`, breaking parsing on Workers — now wraps the Markdown branch only, inside `truncateText`. Added `isError: true` to the two non-dati.gov.it guards. The `truncateJson` fallback now degrades further so it always respects small limits. 454 passing.
|
|
20
|
+
|
|
21
|
+
## 2026-07-09
|
|
22
|
+
|
|
23
|
+
### v0.4.112
|
|
24
|
+
|
|
25
|
+
Security — Round 3: hardening (closes the last group of advisories in triage).
|
|
26
|
+
|
|
27
|
+
- **Error reflection**: `makeCkanRequest` no longer embeds the upstream body (`JSON.stringify(decodedData)`) in the error returned to the caller — now a generic action-scoped message, with detail on stderr only (truncated). `worker.ts` catch-all: removed `error.message` from the JSON-RPC `data` field (server-side logging only). Closes the semi-blind read channel of the SSRF.
|
|
28
|
+
- **postMessage UI** (`resources/datastore-table-ui.ts`): the host origin is pinned from the reply to the `ui/initialize` handshake; messages carrying data are accepted only from that origin, and outbound messages use an explicit target origin (never `'*'`).
|
|
29
|
+
- **Prompt injection on org/group**: extended the c499 containment to the `organization.ts` and `group.ts` renderers — `description` in an untrusted block (`wrapUntrusted`), newlines collapsed in lists.
|
|
30
|
+
- 3 new tests (error no-leak, org/group description fencing); 443 passing. E2e: generic error on 404 (no internal body), normal requests fine. Worker build fine.
|
|
31
|
+
|
|
32
|
+
### v0.4.111
|
|
33
|
+
|
|
34
|
+
Security — Round 2: three distinct low-cost bugs.
|
|
35
|
+
|
|
36
|
+
- **MQA allowlist bypass** (`isValidMqaServer`, `tools/quality.ts`): unanchored regex replaced by URL parsing plus exact host comparison (`dati.gov.it`/`www.dati.gov.it`). Suffix (`dati.gov.it.attacker.com`) and userinfo (`dati.gov.it@attacker.com`) tricks are now rejected.
|
|
37
|
+
- **Decompression bomb / unbounded buffering** (`utils/http.ts`): response size cap (`maxContentLength`/`maxBodyLength` on axios plus a byte check on `arrayBuffer` in the fetch branch, default 32MB) and decompression output cap (`maxOutputLength` on gunzip/brotli/inflateSync plus a check on DecompressionStream, default 64MB). Override via `CKAN_MAX_RESPONSE_BYTES`/`CKAN_MAX_DECOMPRESSED_BYTES`. Stops OOM/stalls from hyper-compressed payloads.
|
|
38
|
+
- **Cache-key collision** (`utils/cache.ts`): `canonicalizeParams` now produces typed canonical JSON (recursive sort) instead of a `k=v` join with unescaped `&`; `buildCacheKey` frames it in `JSON.stringify([url,action,canon])`. `{q:"budget",rows:10}` and `{q:"budget&rows=10"}` no longer collide.
|
|
39
|
+
- 2 new tests (plus updated regressions); 440 passing. E2e: valid MQA host reaches data.europa.eu, bypass rejected, normal requests fine. Worker build fine.
|
|
40
|
+
- Further hardening in progress for upcoming releases.
|
|
41
|
+
|
|
42
|
+
### v0.4.110
|
|
43
|
+
|
|
44
|
+
Security — Round 1: SSRF cluster on the `fetch` path (GHSA-vmrr, GHSA-38f8; GHSA-8hxx clarified):
|
|
45
|
+
|
|
46
|
+
- **Centralized `safeFetch()`** in `utils/http.ts`: `redirect:"manual"` plus re-validation of every hop (`validateServerUrl` + `assertHostnameResolvesSafe`), bounded hops, `httpsOnly` option. Closes redirect-SSRF (e.g. a public endpoint 302-ing to `169.254.169.254`) without breaking legitimate canonical redirects. Used by `sparql_query` (3 fetches) and by the MQA metrics fetch in `quality.ts`.
|
|
47
|
+
- **`assertHostnameResolvesSafe` is now fail-closed**: it distinguishes "DNS module absent (Workers) → no-op" from "resolution failed → throw". Previously a DNS error let the request proceed (fail-open / TOCTOU).
|
|
48
|
+
- **GHSA-8hxx**: verified that WHATWG `URL` already normalizes IPv4 encodings (int/hex/octal/short) to dotted-decimal before `validateServerUrl` → the existing check already covers them. No code added (the advisory PoC tested the regex against raw strings, not the parsed hostname). Only a comment and regression tests. The remaining `::7f00:1` (IPv4-compatible IPv6) is not routable, as the advisory itself concedes.
|
|
49
|
+
- MQA fetch: constant host (`data.europa.eu`), so hardening for consistency rather than a real vector.
|
|
50
|
+
- 6 new tests (redirect→internal blocked, redirect→non-HTTPS rejected, fail-closed on DNS error, IPv4 encodings blocked). 438 passing. E2e: Wikidata via `safeFetch` fine, internal IP blocked. Worker build fine.
|
|
51
|
+
- Further security hardening in progress for upcoming releases.
|
|
52
|
+
|
|
53
|
+
### v0.4.109
|
|
54
|
+
|
|
55
|
+
Security hardening — 3 advisories, "poison and door" (environmental risk before the knives):
|
|
56
|
+
|
|
57
|
+
- **GHSA-3369** (second-order SSRF, `ckan_list_resources`): source-portal probing is now **opt-in** (`check_source_portal` defaults to `false`). It used to be ON: listing a dataset's resources contacted hosts and ports taken from the dataset's own data (confused deputy + port-scan oracle + amplification via `Promise.all`). Added: ports other than 80/443 dropped in `extractSourcePortal` (uses `hostname`, not `host`), fan-out capped at 10 probes.
|
|
58
|
+
- **GHSA-c499** (indirect prompt injection): free-text portal fields (`notes`, resource `description`) were rendered verbatim in the output. They are now wrapped in a delimited `untrusted` block with a warning (`wrapUntrusted`), with inner fences neutralized; portal URLs are scheme-validated (http/https only) and rendered as inline code (`safeUrlText`); `ckan_list_resources` table cells are neutralized (`|`, newlines). Containment, not a complete fix — documented for integrators.
|
|
59
|
+
- **GHSA-v3j5** (exposed HTTP transport): binds to **`127.0.0.1`** by default (was `0.0.0.0`), `enableDnsRebindingProtection` plus `allowedHosts`/`allowedOrigins`. `docker-compose.yml` publishes on `127.0.0.1:3000:3000` (with `CKAN_HTTP_HOST=0.0.0.0` inside the container). New env vars: `CKAN_HTTP_HOST`, `CKAN_HTTP_ALLOWED_HOSTS`, `CKAN_HTTP_ALLOWED_ORIGINS`. Closing this door downgrades the whole SSRF cluster from "remote" to "local".
|
|
60
|
+
- 4 new tests; 432 passing. Verified e2e against a real HTTP deployment (loopback bind, 403 on disallowed Host, no probing by default, notes fenced). Worker build fine.
|
|
61
|
+
- Docs: README (HTTP env var table), SKILL (source-portal now opt-in), docker/README, docker-compose.
|
|
62
|
+
- **Not done yet** (fetch SSRF cluster, MQA regex, cache, decompression bomb, postMessage UI): knives and hardening, in later rounds.
|
|
63
|
+
|
|
3
64
|
## 2026-06-22
|
|
4
65
|
|
|
5
66
|
### v0.4.108
|