Dns Cache Poisoning in Feathersjs with Hmac Signatures
Dns Cache Poisoning in Feathersjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
DNS cache poisoning is a network-layer attack where false DNS responses cause a resolver to cache an incorrect IP mapping for a domain. In FeathersJS applications that rely on service discovery or external hostnames, poisoned DNS entries can redirect traffic to an attacker-controlled host. When Hmac Signatures are used to authenticate requests, the impact depends on how and where the signature is computed.
If the signature is generated over a request payload or selected headers but does not include the resolved destination endpoint (e.g., the final hostname and port used after DNS resolution), an attacker who can poison DNS may redirect the request to a malicious server that presents a valid TLS certificate for the target domain. Because the Hmac Signature is still valid—computed over the unchanged payload and headers—the server will accept the request as authentic. This mismatch between the signed intent and the actual network destination undermines endpoint integrity, even though the cryptographic integrity of the payload is preserved.
Consider a FeathersJS service that calls an external API using a hostname like api.example.com. The client resolves this hostname, caches the poisoned IP, and sends an Hmac-signed request to the malicious server. If the signature excludes the hostname or the resolved IP, the malicious server can forward or alter the request and response without detection. An attacker might intercept or manipulate data, or induce the client to perform unintended actions on a trusted downstream service. This is especially relevant when requests include sensitive operations or tokens that are not themselves cryptographically bound to the endpoint identity.
In FeathersJS, this can occur when using hooks or transports that perform HTTP requests without ensuring the signed material covers the resolved destination. For example, if the Hmac signature is computed from the body and certain headers but omits the URL host and port, poisoned DNS enables a man-in-the-middle scenario where the signature does not protect against redirection to a rogue endpoint. The vulnerability is not in Hmac Signatures per se, but in the incomplete binding between the signed data and the network destination, which DNS poisoning can exploit.
To illustrate, a client that signs only the JSON body and a timestamp leaves the resolved hostname outside the protected scope. An attacker who successfully poisons DNS to point api.example.com to 10.0.0.1 can serve any certificate that passes TLS validation (e.g., a certificate for the legitimate domain issued by a compromised CA or via Server Name Indication manipulation). The Hmac-signed request will still verify, because the payload and headers are unchanged, but the request reaches an unintended server.
Therefore, to mitigate DNS cache poisoning in this context, the Hmac signature must incorporate the endpoint hostname and, where relevant, the resolved IP or a certificate fingerprint. This ensures that any change in the network destination invalidates the signature, preventing acceptance of requests redirected by a poisoned cache.
Hmac Signatures-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring the Hmac signature covers the full request context, including the resolved hostname and, where appropriate, the path and query parameters. In FeathersJS, this typically involves modifying the client-side request hook that produces the Hmac signature to include the final URL used for the HTTP call.
Below is a syntactically correct example of an Hmac Signature client hook for FeathersJS that includes the hostname and pathname in the signed string. This approach binds the signature to the endpoint, reducing the risk that DNS poisoning will lead to accepted malicious requests.
const crypto = require('crypto');
class HmacSignatureClient {
constructor(app, options = {}) {
this.app = app;
this.name = options.name || 'external-service';
this.secret = options.secret || 'shared-secret';
this.baseURL = options.baseURL || 'https://api.example.com';
}
sign(method, path, timestamp, body) {
const payload = [
this.name,
method.toUpperCase(),
path,
timestamp,
body ? JSON.stringify(body) : ''
].join('|');
return crypto.createHmac('sha256', this.secret).update(payload).digest('hex');
}
getURL(path) {
// Ensure consistent URL construction
const url = new URL(path, this.baseURL);
return url.toString();
}
computeSignature(method, url, timestamp, body) {
const parsed = new URL(url);
// Include hostname and pathname in the signed material
const pathForSigning = parsed.pathname + (parsed.search || '');
const message = [
this.name,
method.toUpperCase(),
parsed.hostname,
pathForSigning,
timestamp,
body ? JSON.stringify(body) : ''
].join('|');
return crypto.createHmac('sha256', this.secret).update(message).digest('hex');
}
async request(options) {
const timestamp = Date.now().toString();
const url = this.getURL(options.path);
const signature = this.computeSignature(options.method || 'GET', url, timestamp, options.body);
const headers = {
'X-API-Timestamp': timestamp,
'X-API-Signature': signature,
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
return res.json();
}
}
// Usage within a FeathersJS hook
const hmacClient = new HmacSignatureClient(app, {
name: 'tickets-service',
secret: process.env.HMAC_SECRET,
baseURL: process.env.EXTERNAL_API_URL
});
app.hooks.before.push(async context => {
if (context.method === 'create') {
const signedData = await hmacClient.request({
method: 'POST',
path: '/tickets/validate',
body: context.data
});
context.params.signedValidation = signedData;
}
return context;
});
The key security improvement is including parsed.hostname in the signed message. This ensures that if DNS is poisoned and the request reaches a different IP, the signature will not match when verified by the server, which must use the same hostname-to-signature binding. The server-side verification should reconstruct the signature using the hostname from the request (or an expected hostname) and reject the request if it does not match.
For completeness, here is a corresponding server-side verification snippet in Node.js style that demonstrates how the service should validate the Hmac signature using the hostname from the request (or a trusted allowlist). This prevents acceptance of requests with mismatched destinations even if DNS is poisoned and the request arrives at a server controlled by the attacker.
const crypto = require('crypto');
function verifyHmacSignature(message, receivedSignature, secret) {
const expected = crypto.createHmac('sha256', secret).update(message).digest('hex');
// Use timing-safe compare
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
}
function validateRequest(req, res, next) {
const { 'x-api-timestamp': timestamp, 'x-api-signature': signature } = req.headers;
const hostname = req.hostname; // or a trusted hostname from configuration
const path = req.originalUrl.split('?')[0];
const body = req.body;
const payload = [
'tickets-service',
req.method.toUpperCase(),
hostname,
path,
timestamp,
body ? JSON.stringify(body) : ''
].join('|');
if (!verifyHmacSignature(payload, signature, process.env.HMAC_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Additional check: ensure hostname matches expected service
const allowedHostnames = ['api.example.com'];
if (!allowedHostnames.includes(hostname)) {
return res.status(403).json({ error: 'Hostname not allowed' });
}
next();
}
By incorporating the hostname into the Hmac signature and validating it server-side, you ensure that even if DNS cache poisoning redirects the request, the signature will fail to verify for the attacker’s endpoint, effectively neutralizing the poisoning attempt against authenticated API calls.