Dns Cache Poisoning in Express with Bearer Tokens
Dns Cache Poisoning in Express with Bearer Tokens — how this specific combination creates or exposes the vulnerability
DNS cache poisoning (also known as DNS spoofing) occurs when an attacker injects forged DNS responses into a resolver’s cache, causing a domain to resolve to an attacker-controlled IP. In an Express application that relies on outbound HTTP requests to services discovered via DNS, poisoned cache can redirect traffic to a malicious host. When bearer tokens are used for authorization, this redirection can expose credentials and session material.
Consider an Express service that uses a hostname (e.g., api.partner.example) to reach an upstream API, sending requests with an Authorization: Bearer header. If the application does not pin the resolved IP or validate the server’s identity, a poisoned DNS entry can point the request to an attacker server that terminates TLS with a valid certificate for the hostname (e.g., via a compromised CA or a valid wildcard cert). Because the bearer token is attached automatically by the client, the malicious server can capture the token and replay it to impersonate the original service or escalate privileges.
The risk is compounded when Express is used in backend-to-backend flows where tokens are long-lived and permissions are broad. An attacker who controls DNS within the network or compromises a recursive resolver can intercept service discovery or API calls, leading to token theft and unauthorized access. This is a network-layer issue, but its impact is realized in the application layer when bearer tokens are transmitted to an unintended endpoint.
Bearer Tokens-Specific Remediation in Express — concrete code fixes
To reduce exposure, minimize the lifetime of bearer tokens in Express-based clients and avoid sending high-privilege tokens to hosts that cannot be strongly authenticated. Use short-lived tokens and refresh them using secure mechanisms. When calling external services, prefer explicit IP connections or certificate pinning where feasible, and validate the server’s identity before transmitting credentials.
Code example: Safe bearer token usage with short-lived access tokens
const express = require('express');
const axios = require('axios');
const qs = require('querystring');
const app = express();
app.use(express.json());
// Exchange a refresh token for a short-lived access token
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'missing_credentials' });
}
try {
const params = new URLSearchParams();
params.append('grant_type', 'password');
params.append('username', username);
params.append('password', password);
params.append('client_id', process.env.CLIENT_ID);
params.append('client_secret', process.env.CLIENT_SECRET);
const tokenResp = await axios.post('https://auth.example.com/oauth/token', params.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
res.json({
access_token: tokenResp.data.access_token,
expires_in: tokenResp.data.expires_in
});
} catch (err) {
res.status(502).json({ error: 'auth_failure' });
}
});
// Use short-lived access token to call a downstream service
app.get('/data', async (req, res) => {
const accessToken = req.headers.authorization?.replace('Bearer ', '');
if (!accessToken) {
return res.status(401).json({ error: 'no_token' });
}
try {
// Prefer explicit HTTPS endpoint; avoid relying on DNS at call time if you can pin IPs/certs
const apiResponse = await axios.get('https://api.partner.example/v1/resource', {
headers: { Authorization: `Bearer ${accessToken}` },
httpsAgent: new (require('https').Agent)({ keepAlive: true }),
timeout: 5000
});
res.json(apiResponse.data);
} catch (err) {
res.status(502).json({ error: 'upstream_error' });
}
});
module.exports = app;
Code example: Centralized request wrapper with hostname verification
const axios = require('axios');
const https = require('https');
const crypto = require('crypto');
// Pin the expected certificate fingerprint for api.partner.example
const EXPECTED_FINGERPRINT = 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12';
function verifyFingerprint(res) {
const peerCert = res.connection.getPeerCertificate();
if (!peerCert || !peerCert.fingerprint) {
throw new Error('missing_peer_certificate');
}
const fingerprint = peerCert.fingerprint.replace(/:/g, '').toUpperCase();
const normalized = fingerprint.replace(/:/g, '').match(/.{1,2}/g).join(':').toUpperCase();
if (normalized !== EXPECTED_FINGERPRINT) {
throw new Error('fingerprint_mismatch');
}
}
const client = axios.create({
baseURL: 'https://api.partner.example',
timeout: 8000,
httpsAgent: new https.Agent({
rejectUnauthorized: true,
// In production, use a custom check that validates fingerprint or pinned public key
})
});
client.interceptors.response.use((response) => {
verifyFingerprint(response);
return response;
}, (error) => {
return Promise.reject(error);
});
// Example usage in an Express route
const express = require('express');
const app = express();
app.get('/protected', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'token_required' });
try {
const response = await client.get('/v1/secure', {
headers: { Authorization: `Bearer ${token}` }
});
res.json(response.data);
} catch (err) {
res.status(502).json({ error: err.message || 'upstream_error' });
}
});
module.exports = app;