Overview
The CVE-2026-34360 vulnerability describes a server-side request forgery (SSRF) flaw in HAPI FHIR's Validator HTTP service. Prior to version 6.9.4, the /loadIG endpoint accepted a user-supplied URL via a JSON body and performed server-side HTTP requests to that URL without validating the hostname, scheme, or domain. An unauthenticated attacker with network access could trigger outbound requests to internal network services, cloud metadata endpoints, and other protected resources. With explore=true enabled by default, each request could spawn multiple outbound calls, increasing reconnaissance potential and enabling topology mapping through error-based leakage. The issue was patched in version 6.9.4 (CWE-918).
Affected Versions
<6.9.4 (HAPI FHIR Validator HTTP service) - CVE-2026-34360
Code Fix Example
Hapi API Security Remediation
/* Vulnerable pattern (no host URL validation, arbitrary outbound fetch) */
// Using Hapi.js style route example for clarity
const Hapi = require('@hapi/hapi');
const fetch = require('node-fetch');
async function startVulnerable() {
const server = Hapi.server({ port: 3000, host: '0.0.0.0' });
server.route({
method: 'POST',
path: '/loadIG',
handler: async (request, h) => {
const { url } = request.payload; // attacker-controlled URL
const res = await fetch(url); // SSRF risk: outbound request to arbitrary URL
const data = await res.text();
return data;
}
});
await server.start();
console.log('Server running', server.info.uri);
}
startVulnerable();
/* Fixed pattern (validate and restrict URL, implement allowlist and egress controls) */
const dns = require('dns').promises;
function isPrivateIP(ip) {
if (!ip) return false;
const parts = ip.split('.').map(Number);
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return false;
const [a,b,c,d] = parts;
if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 127) return true;
return false;
}
const allowedHosts = ['example.com','api.example.com'];
async function isHostAllowed(hostname) {
if (!allowedHosts.includes(hostname)) return false;
try {
const addrs = await dns.lookup(hostname, { all: true });
for (const a of addrs) {
if (isPrivateIP(a.address)) return false;
}
} catch {
return false;
}
return true;
}
async function startFixed() {
const server = Hapi.server({ port: 3001, host: '0.0.0.0' });
server.route({
method: 'POST',
path: '/loadIG',
handler: async (request, h) => {
const { url } = request.payload;
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Unsupported protocol');
if (!(await isHostAllowed(parsed.hostname))) throw new Error('Host not allowed');
// Resolve hostname and ensure no private IPs are involved
const addrs = await dns.lookup(parsed.hostname, { all: true });
if (addrs.some(a => isPrivateIP(a.address))) throw new Error('Private IPs are blocked');
const res = await fetch(url, { timeout: 5000 });
const data = await res.text();
return data;
} catch (e) {
return h.response({ error: e.message }).code(400);
}
}
});
await server.start();
console.log('Server running', server.info.uri);
}
startFixed();