Dangling Dns in Hapi with Mutual Tls
Dangling Dns in Hapi with Mutual Tls — how this specific combination creates or exposes the vulnerability
A Dangling DNS configuration in a Hapi server combined with Mutual TLS (mTLS) can expose the unauthenticated attack surface during the 5–15 second scan window. In Hapi, the server may resolve a hostname (e.g., internal service or metadata service) at route or plugin initialization time, or lazily on request. If TLS client authentication is enforced but the DNS resolution occurs before mTLS peer verification completes, an attacker who can influence the hostname (via path, query, or header) may cause the server to connect to an unintended internal endpoint. Because mTLS verifies the client certificate, the server trusts the client’s presented identity, but it does not inherently validate that the hostname it resolves aligns with the intended service. When the resolved DNS name points to an internal or external service not expected by the API, data may be sent to an unauthorized host, or sensitive responses may be returned to the client depending on how the server handles the upstream result.
During a black-box scan, middleBrick tests unauthenticated endpoints and checks whether user-controlled input can lead to off-host requests or data exposure. If a Hapi route uses a hostname from user input (e.g., a webhook URL or service discovery value) and performs DNS resolution at request time with mTLS enabled, the scan can detect whether the resolved target is internal or whether responses from unintended hosts are returned. This is especially relevant when the server does not validate the hostname against the certificate’s subject or SAN, allowing a mismatch that results in data being routed to an external or internal unintended peer. The combination therefore creates a risk where enforced mTLS gives a false sense of security while DNS resolution diverts traffic to unintended endpoints, potentially leaking data or enabling SSRF-like behaviors within the mTLS trust boundary.
For example, if a route accepts a hostname header and uses Node’s DNS lookup without additional validation, the server may resolve an internal service name that is not part of the expected service mesh. With mTLS enabled, the client certificate is verified, but the server does not ensure the resolved hostname matches the certificate’s intended target. middleBrick’s checks include testing whether user-controlled inputs can change network destinations and whether responses from unintended hosts are returned, which helps surface this class of configuration issue.
Mutual Tls-Specific Remediation in Hapi — concrete code fixes
Remediation focuses on ensuring DNS resolution aligns with mTLS expectations and that hostnames are validated against the peer certificate. Do not derive target hostnames from untrusted input. If you must resolve dynamic services, validate the resolved address against an allowlist and ensure the hostname used for TLS matches the certificate’s subject or SAN. Below are concrete Hapi examples that demonstrate safe patterns.
Example 1: Strict mTLS server with static target
Configure the server to use a fixed hostname for upstream calls and enforce client certificate verification without relying on dynamic DNS-derived targets.
const Hapi = require('@hapi/hapi');
const tls = require('tls');
const init = async () => {
const server = Hapi.server({
port: 443,
tls: {
key: fs.readFileSync('service.key'),
cert: fs.readFileSync('service.crt'),
ca: fs.readFileSync('ca.crt'),
requestCert: true,
rejectUnauthorized: true,
},
});
server.route({
method: 'POST',
path: '/report',
handler: (request, h) => {
// Use a statically defined, validated endpoint
const target = 'https://internal-reporter.example.com/ingest';
// Perform request with mTLS using fixed hostname
return tls.request({
hostname: 'internal-reporter.example.com',
port: 443,
method: 'POST',
cert: fs.readFileSync('service.key'),
key: fs.readFileSync('service.key'),
ca: fs.readFileSync('ca.crt'),
servername: 'internal-reporter.example.com',
}, (res) => {
// handle response
}).on('error', (err) => {
request.logger.error(err);
});
},
});
await server.start();
};
Example 2: Validating dynamic hostnames against an allowlist
If you must resolve a hostname, resolve it, then confirm it belongs to an allowed set before using it in TLS options. Do not pass the raw user-supplied value to DNS or TLS configuration.
const Hapi = require('@hapi/hapi');
const dns = require('dns').promises;
const ALLOWED_HOSTS = new Set([
'internal-reporter.example.com',
'metrics.internal.example.com',
]);
const init = async () => {
const server = Hapi.server({
port: 443,
tls: {
key: fs.readFileSync('service.key'),
cert: fs.readFileSync('service.crt'),
ca: fs.readFileSync('ca.crt'),
requestCert: true,
rejectUnauthorized: true,
},
});
server.route({
method: 'POST',
path: '/report/{host}',
async handler(request, h) {
const host = request.params.host;
if (!ALLOWED_HOSTS.has(host)) {
return h.response({ error: 'forbidden host' }).code(403);
}
// Verify DNS resolution matches expected host
const resolved = await dns.resolve4(host);
if (resolved.length === 0) {
return h.response({ error: 'cannot resolve' }).code(400);
}
// Use servername matching the validated hostname
const tlsOptions = {
hostname: host,
port: 443,
method: 'POST',
cert: fs.readFileSync('service.key'),
key: fs.readFileSync('service.key'),
ca: fs.readFileSync('ca.crt'),
servername: host,
};
// Perform request with validated hostname
return new Promise((resolve, reject) => {
const req = tls.request(tlsOptions, (res) => {
resolve({ statusCode: res.statusCode });
});
req.on('error', reject);
req.end();
});
},
});
await server.start();
};
Best practices summary
- Do not use user-controlled input to construct hostnames for TLS requests.
- Pin the servername in TLS options and ensure it matches the certificate’s subject or SAN.
- Validate resolved DNS results against an allowlist before use.
- Keep client certificate verification enabled (requestCert + rejectUnauthorized) and ensure the CA bundle is up to date.