Summary
ckan_list_resources (with check_source_portal, default on) derives the destination of a server-side HTTP request from response data rather than from caller input. The host, port and scheme are taken from the url field of a dataset's resources — metadata controlled by whoever published the dataset — so a caller who merely lists the resources of a malicious dataset causes the server to issue requests to an attacker-chosen origin. This is a single defect: an outbound destination is constructed from untrusted data without being constrained to the trust boundary the rest of the server enforces (server_url / CKAN_ALLOWED_DOMAINS).
Affected code
src/tools/package.ts — the probe is issued against a host that never passed through caller-controlled validation:
if (doSourceCheck) {
await Promise.all(
summary.map(async (item, idx) => {
if (item.datastore_active) return;
const extracted = extractSourcePortal(item.url, params.server_url); // host from DATA
if (!extracted) return;
const active = await checkSourceDatastore(extracted.portalUrl, extracted.resourceId);
summary[idx].source_datastore_active = active; // boolean oracle returned to caller
summary[idx].source_portal_url = active ? extracted.portalUrl : null;
})
);
}
async function checkSourceDatastore(portalUrl, resourceId) {
try {
await makeCkanRequest(portalUrl, 'datastore_search', { resource_id: resourceId, limit: 0 }, { cache: false });
return true;
} catch { return false; }
}
src/utils/url-generator.ts — the destination is built directly from the resource URL, including its port:
export function extractSourcePortal(resourceUrl, serverUrl) {
...
if (rParsed.hostname === sParsed.hostname) return null;
const match = rParsed.pathname.match(UUID_RE); // /resource/<uuid>
if (!match) return null;
return { portalUrl: `${rParsed.protocol}//${rParsed.host}`, resourceId: match[1] };
}
Any resource whose url is http(s)://<attacker-host>:<port>/resource/<uuid> (and whose host differs from server_url) becomes an outbound request target.
Impact
The consequences below all follow from the same unvalidated data-derived destination:
- Confused-deputy request forgery. The trust model elsewhere assumes the caller chooses the destination via
server_url. Here the destination comes from third-party data, so publishing a crafted dataset on any portal the victim queries — or operating a portal the victim queries — makes the victim's server contact arbitrary external origins on the attacker's behalf.
- Arbitrary port → external port scanning / service probing.
extractSourcePortal preserves the port, and the tool returns a boolean (source_datastore_active) plus timing, giving the caller an oracle for whether the server can reach host:port.
- Default-on and low friction.
check_source_portal defaults to true; the caller need only list a dataset's resources.
Internal targets remain blocked by the existing guards (validateServerUrl literal check + createSsrfSafeLookup DNS pinning), so the exposure is external request forgery and probing. It is most impactful on stdio deployments (the common desktop case), which run with no domain allowlist by default; when CKAN_ALLOWED_DOMAINS is set (mandatory only for the HTTP transport) the destination is constrained to allowlisted hosts.
Scope note (CVSS S:C): the server is induced to act against other systems (request forgery and probing directed at third parties).
Proof of concept
poc/data-driven-ssrf-poc.mjs uses the verbatim extractSourcePortal logic:
== destination host:port is taken from attacker-controlled dataset data ==
resource.url=http://third-party-victim.example:2222/resource/<uuid>
-> outbound target: http://third-party-victim.example:2222 guard-allows=true
Remediation
Constrain the derived destination to the caller's trust boundary — one control, applied where portalUrl is constructed:
- Do not derive outbound destinations from response data by default. Gate
check_source_portal behind an explicit opt-in, and/or restrict source-portal probing to hosts on CKAN_ALLOWED_DOMAINS (or the same host as server_url).
- Drop non-default ports (or allow only 80/443) when constructing
portalUrl, so the destination cannot be steered to arbitrary services.
Summary
ckan_list_resources(withcheck_source_portal, default on) derives the destination of a server-side HTTP request from response data rather than from caller input. The host, port and scheme are taken from theurlfield of a dataset's resources — metadata controlled by whoever published the dataset — so a caller who merely lists the resources of a malicious dataset causes the server to issue requests to an attacker-chosen origin. This is a single defect: an outbound destination is constructed from untrusted data without being constrained to the trust boundary the rest of the server enforces (server_url/CKAN_ALLOWED_DOMAINS).Affected code
src/tools/package.ts— the probe is issued against a host that never passed through caller-controlled validation:src/utils/url-generator.ts— the destination is built directly from the resource URL, including its port:Any resource whose
urlishttp(s)://<attacker-host>:<port>/resource/<uuid>(and whose host differs fromserver_url) becomes an outbound request target.Impact
The consequences below all follow from the same unvalidated data-derived destination:
server_url. Here the destination comes from third-party data, so publishing a crafted dataset on any portal the victim queries — or operating a portal the victim queries — makes the victim's server contact arbitrary external origins on the attacker's behalf.extractSourcePortalpreserves the port, and the tool returns a boolean (source_datastore_active) plus timing, giving the caller an oracle for whether the server can reachhost:port.check_source_portaldefaults to true; the caller need only list a dataset's resources.Internal targets remain blocked by the existing guards (
validateServerUrlliteral check +createSsrfSafeLookupDNS pinning), so the exposure is external request forgery and probing. It is most impactful on stdio deployments (the common desktop case), which run with no domain allowlist by default; whenCKAN_ALLOWED_DOMAINSis set (mandatory only for the HTTP transport) the destination is constrained to allowlisted hosts.Scope note (CVSS
S:C): the server is induced to act against other systems (request forgery and probing directed at third parties).Proof of concept
poc/data-driven-ssrf-poc.mjsuses the verbatimextractSourcePortallogic:Remediation
Constrain the derived destination to the caller's trust boundary — one control, applied where
portalUrlis constructed:check_source_portalbehind an explicit opt-in, and/or restrict source-portal probing to hosts onCKAN_ALLOWED_DOMAINS(or the same host asserver_url).portalUrl, so the destination cannot be steered to arbitrary services.