@aborruso/ckan-mcp-server 0.4.117 → 0.4.118

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.
Files changed (3) hide show
  1. package/LOG.md +16 -0
  2. package/dist/index.js +23 -2
  3. package/package.json +4 -3
package/LOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-08-06
4
+
5
+ ### v0.4.118 — pin the fetch path to the validated IP
6
+
7
+ `GHSA-r8hw-3fch-r42w` reported the v0.4.108 SSRF fix as bypassable by DNS rebinding, with a PoC pointing `ckan_package_search` at `7f000001.7f000001.rbndr.us`. Reproduced: the PoC is blocked. Both halves of that label decode to 127.0.0.1, so the name never rebinds — it is a plain DNS-name-to-loopback, the case v0.4.108 already closed. Replaying it against `localtest.me` with a listener on 127.0.0.1:8054 is rejected by `createSsrfSafeLookup`, and a genuine rebinding fake-DNS (public IP first, loopback second) through the axios agent produces exactly one lookup with the connection pinned to the validated address.
8
+
9
+ The class was right on the wrong path, though. `safeFetch()` — used by `sparql_query` and the MQA quality tools — validated the hostname with `assertHostnameResolvesSafe()` and then let undici resolve it a second time, which is a real TOCTOU window. `getSafeDispatcher()` now builds an `undici.Agent({ connect: { lookup: createSsrfSafeLookup(dns) } })` and `safeFetch` passes it on every hop, so the socket connects to the address that was just checked. `assertHostnameResolvesSafe()` stays as defence in depth for runtimes without a dispatcher (Workers, where the CF sandbox blocks internal egress anyway).
10
+
11
+ `undici` becomes an explicit dependency: it was only present transitively through wrangler, i.e. absent in production installs. The import specifier is assembled at runtime (`["und","ici"].join("")`) because esbuild constant-folds `"undici" + ""` and would otherwise bundle all of undici into `dist/index.js` (207 KB → 969 KB) and break the browser-platform Workers build.
12
+
13
+ Same trap on the other side: because the specifier is dynamic, the DXT bundle cannot contain undici either, and a `.dxt` unpacked by Claude Desktop has no `node_modules` to resolve it from — the dispatcher would have been null and the pin silently inert. `pack:dxt` now copies `node_modules/undici` into `dxt-staging/server/node_modules/` (+1.7 MB uncompressed), verified by resolving it from a copy of the staging dir outside the repo.
14
+
15
+ `engines.node` moves to `>=18.17.0`, undici 6's own floor — the old `>=18.0.0` would have promised installs that cannot work.
16
+
17
+ Also fixed the `serverInfo` version, hardcoded at `0.4.108` in `src/server.ts` and `src/worker.ts` while the package was at 0.4.117.
18
+
3
19
  ## 2026-08-04
4
20
 
5
21
  ### v0.4.117 — escaping portal-controlled strings
package/dist/index.js CHANGED
@@ -776,6 +776,22 @@ function getSafeAgents() {
776
776
  }
777
777
  return _safeAgents;
778
778
  }
779
+ var _safeDispatcher = null;
780
+ function getSafeDispatcher() {
781
+ if (!_safeDispatcher) {
782
+ _safeDispatcher = (async () => {
783
+ try {
784
+ const dnsMod = await import("node:dns");
785
+ const undiciSpecifier = ["und", "ici"].join("");
786
+ const undiciMod = await import(undiciSpecifier);
787
+ return new undiciMod.Agent({ connect: { lookup: createSsrfSafeLookup(dnsMod) } });
788
+ } catch {
789
+ return null;
790
+ }
791
+ })();
792
+ }
793
+ return _safeDispatcher;
794
+ }
779
795
  var _dnsResolver = null;
780
796
  async function assertHostnameResolvesSafe(hostname) {
781
797
  let lookup;
@@ -806,9 +822,14 @@ async function assertHostnameResolvesSafe(hostname) {
806
822
  }
807
823
  async function safeFetch(url, init = {}, opts = {}) {
808
824
  const maxHops = opts.maxHops ?? 3;
825
+ const dispatcher = await getSafeDispatcher();
809
826
  let current = url;
810
827
  for (let hop = 0; ; hop++) {
811
- const response = await fetch(current, { ...init, redirect: "manual" });
828
+ const response = await fetch(current, {
829
+ ...init,
830
+ redirect: "manual",
831
+ ...dispatcher ? { dispatcher } : {}
832
+ });
812
833
  const status = response.status;
813
834
  const location = response.headers.get("location");
814
835
  const isRedirect = (status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && !!location;
@@ -5736,7 +5757,7 @@ var registerAllPrompts = (server) => {
5736
5757
  function createServer() {
5737
5758
  return new McpServer({
5738
5759
  name: "ckan-mcp-server",
5739
- version: "0.4.108"
5760
+ version: "0.4.118"
5740
5761
  });
5741
5762
  }
5742
5763
  function registerAll(server) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.117",
3
+ "version": "0.4.118",
4
4
  "mcpName": "io.github.aborruso/ckan-mcp-server",
5
5
  "description": "MCP server for interacting with CKAN open data portals",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "test:watch": "vitest --watch",
26
26
  "test:coverage": "vitest --coverage",
27
27
  "build:dxt": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dxt-staging/server/index.js",
28
- "pack:dxt": "rm -rf dxt-staging && mkdir -p dxt-staging/server && npm run build:dxt && echo '{\"type\":\"commonjs\"}' > dxt-staging/server/package.json && cp manifest.json icon.png dxt-staging/ && dxt pack dxt-staging ckan-mcp-server.dxt && rm -rf dxt-staging",
28
+ "pack:dxt": "rm -rf dxt-staging && mkdir -p dxt-staging/server && npm run build:dxt && echo '{\"type\":\"commonjs\"}' > dxt-staging/server/package.json && mkdir -p dxt-staging/server/node_modules && cp -r node_modules/undici dxt-staging/server/node_modules/ && cp manifest.json icon.png dxt-staging/ && dxt pack dxt-staging ckan-mcp-server.dxt && rm -rf dxt-staging",
29
29
  "pack:skill": "mkdir -p tmp && cd skills && zip -r ../tmp/ckan-mcp.skill ckan-mcp/ && cd ..",
30
30
  "prepack": "cp README.md .readme-full.md && cp .readme-npm.md README.md",
31
31
  "postpack": "mv .readme-full.md README.md"
@@ -44,6 +44,7 @@
44
44
  "@modelcontextprotocol/sdk": "^1.27.1",
45
45
  "axios": "^1.7.2",
46
46
  "express": "^4.19.2",
47
+ "undici": "^6.28.0",
47
48
  "zod": "^3.23.8"
48
49
  },
49
50
  "devDependencies": {
@@ -70,6 +71,6 @@
70
71
  "AGENTS.md"
71
72
  ],
72
73
  "engines": {
73
- "node": ">=18.0.0"
74
+ "node": ">=18.17.0"
74
75
  }
75
76
  }