Dangling Dns in Koa with Hmac Signatures
Dangling Dns in Koa with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A dangling DNS record occurs when a hostname (e.g., internal.api.example.com) previously pointed to an infrastructure target but now resolves to an unintended or external host. In Koa applications that use Hmac Signatures for request authentication, this combination can expose internal or restricted endpoints if signature validation relies only on the request target without ensuring the resolved endpoint is intended.
Consider a Koa service that validates Hmac signatures to authorize requests to internal administrative routes. The server computes an Hmac over selected parts of the request (method, path, selected headers, and body) using a shared secret. If the application resolves a hostname used in a route or a forwarded URL via DNS at runtime and does not verify that the resolved IP or hostname belongs to an allowed set, an attacker who can influence DNS (e.g., via compromised registrar, internal DHCP/DNS misconfig, or poisoned resolver) can make internal.api.example.com point to a server they control. Because the Hmac is computed over the original URL path and headers the attacker can craft a valid signature for a request that appears to originate from a trusted source, yet the request is routed to the attacker’s dangling host. This bypasses intended network segmentation and may allow the attacker to interact with internal services that the Koa app trusts based on hostname or IP.
In a black-box scan, middleBrick tests such scenarios by submitting requests that include DNS-influenced endpoints where appropriate, using the unauthenticated attack surface to detect whether signature validation alone is insufficient to prevent unintended routing. A misconfigured CNAME or stale A record can cause the application to forward sensitive operations to an external or malicious endpoint, leading to data exposure or SSRF-like outcomes. The scan checks whether Hmac-based integrity controls are coupled with explicit hostname allowlists or strict certificate/pinning checks, which are necessary to mitigate risks from dangling DNS.
Example of a vulnerable Koa route that builds a URL from user input and validates Hmac without ensuring the resolved host is safe:
const Koa = require('koa');
const crypto = require('crypto');
const app = new Koa();
const SHARED_SECRET = process.env.SHARED_SECRET;
function verifyHmac(req) {
const signature = req.headers['x-signature'];
const payload = `${req.method}:${req.path}`;
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.use(async (ctx) => {
// Vulnerable: hostname derived from user-controlled input
const targetHost = ctx.query.host; // e.g., internal.api.example.com
const url = `https://${targetHost}/admin`;
if (!verifyHmac(ctx.req)) {
ctx.status = 401;
return;
}
// Further logic that may use url
ctx.body = { url };
});
module.exports = app;Hmac Signatures-Specific Remediation in Koa — concrete code fixes
To prevent dangling DNS issues in Koa when using Hmac Signatures, you must ensure that the endpoint used in signed operations is explicitly validated and not derived from untrusted input. Use an allowlist of permitted hosts, resolve and verify the hostname against the allowlist, and avoid building URLs from user-controlled data without strict checks.
Below are concrete, secure code examples for Koa that demonstrate how to implement Hmac authentication safely while mitigating dangling DNS risks.
1. Hmac verification with explicit host allowlist
Define a set of allowed hosts and ensure the request target matches one of them before computing or verifying the Hmac. This prevents resolution of arbitrary hostnames.
const Koa = require('koa');
const crypto = require('crypto');
const app = new Koa();
const SHARED_SECRET = process.env.SHARED_SECRET;
const ALLOWED_HOSTS = new Set(['api.example.com', 'internal.api.example.com']);
function verifyHmac(req, host) {
const signature = req.headers['x-signature'];
const payload = `${req.method}:/api${req.path}`;
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
const provided = Buffer.from(signature, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
// timing-safe compare
if (!crypto.timingSafeEqual(provided, expectedBuf)) {
return false;
}
// Ensure the host used is explicitly allowed
return ALLOWED_HOSTS.has(host);
}
app.use(async (ctx) => {
const host = ctx.hostname; // from the request Host header, already validated by Koa
if (!verifyHmac(ctx.req, host)) {
ctx.status = 401;
ctx.body = { error: 'invalid signature' };
return;
}
// Safe: host is from the allowlist and signature is valid
ctx.body = { message: 'authorized' };
});
module.exports = app;
2. Avoid building URLs from user input; use static routing
Do not construct target URLs from query parameters or headers. If proxying or making outbound calls, resolve the destination once at configuration time and validate against a fixed list.
const Koa = require('koa');
const crypto = require('crypto');
const https = require('https');
const SHARED_SECRET = process.env.SHARED_SECRET;
const ALLOWED_PATHS = new Set(['/admin/reset', '/admin/status']);
const FIXED_HOST = 'internal.api.example.com';
function verifyHmac(ctx) {
const signature = ctx.headers['x-signature'];
const payload = `${ctx.method}:${ctx.path}`;
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
app.use(async (ctx) => {
if (!ALLOWED_PATHS.has(ctx.path)) {
ctx.status = 404;
return;
}
if (!verifyHmac(ctx)) {
ctx.status = 401;
ctx.body = { error: 'invalid signature' };
return;
}
// Safe outbound call to a fixed, pre-approved host
const options = {
hostname: FIXED_HOST,
path: ctx.path,
method: ctx.method,
headers: { 'x-signature': ctx.headers['x-signature'] }
};
// Example HTTPS request (not executed in this snippet)
// https.request(options, ...).end();
ctx.body = { forwardedTo: FIXED_HOST + ctx.path };
});
module.exports = app;
3. Validate DNS resolution when hostnames must be dynamic
If dynamic host resolution is required (e.g., multi-tenant routing), resolve the hostname once, cache the mapping, and verify it against a strict allowlist or certificate check before use. Do not trust DNS at request time alone.
const Koa = require('koa');
const crypto = require('crypto');
const dns = require('dns').promises;
const SHARED_SECRET = process.env.SHARED_SECRET;
const ALLOWED_MAPPINGS = {
'service-a.example.com': 'https://10.0.0.1',
'service-b.example.com': 'https://10.0.0.2'
};
async function resolveAndValidate(hostname) {
const resolved = await dns.lookup(hostname);
const mapped = ALLOWED_MAPPINGS[hostname];
if (!mapped) throw new Error('not allowed');
// Optionally validate resolved IP matches expected
return mapped;
}
app.use(async (ctx) => {
const hostname = ctx.hostname;
let target;
try {
target = await resolveAndValidate(hostname);
} catch {
ctx.status = 403;
ctx.body = { error: 'hostname not permitted' };
return;
}
if (!verifyHmac(ctx, hostname)) {
ctx.status = 401;
ctx.body = { error: 'invalid signature' };
return;
}
ctx.body = { target };
});
module.exports = app;