Phishing Api Keys in Express with Hmac Signatures
Phishing API Keys in Express with HMAC Signatures — how this specific combination creates or exposes the vulnerability
Using HMAC signatures in Express to authenticate requests is intended to verify integrity and origin by signing a payload with a shared secret. When API keys are embedded in client-side code, transmitted in URLs, or stored in insecure locations, they become targets for phishing. An attacker can craft a convincing frontend or email that tricks a user into making requests that include both the API key and a valid HMAC signature, enabling unauthorized access that appears legitimate to the server.
The vulnerability arises when the server trusts the API key present in the request without ensuring that the requester is an authorized client. For example, if the client computes an HMAC over a request payload using the shared secret and sends both the key and the signature, a phishing site can replicate this process if it can obtain or guess the key. Even when HMAC prevents tampering, it does not prevent key disclosure via social engineering or insecure storage, allowing attackers to sign arbitrary requests on behalf of the victim.
In Express, a typical insecure pattern is to validate only the HMAC while neglecting to bind the API key to a specific scope, IP, or session. Consider an endpoint that expects X-API-Key and X-Signature headers:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.SHARED_SECRET;
function verifyHmac(req, res, next) {
const key = req.headers['x-api-key'];
const receivedSignature = req.headers['x-signature'];
if (!key || !receivedSignature) {
return res.status(401).json({ error: 'Missing key or signature' });
}
const hmac = crypto.createHmac('sha256', SHARED_SECRET);
hmac.update(JSON.stringify(req.body));
const expectedSignature = hmac.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(receivedSignature), Buffer.from(expectedSignature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Missing: ensure the key is authorized for this endpoint/scope
next();
}
app.post('/transfer', verifyHmac, (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);
If the API key is phished, the attacker can compute a valid HMAC for any request body using the leaked secret (if derivable) or reuse a captured signature. Without additional context such as rate limiting, strict key-to-client binding, or short-lived tokens, the combination of Express, HMAC-based integrity checks, and exposed API keys creates a phishing vector where stolen credentials grant access that appears fully authenticated to the server.
HMAC Signatures-Specific Remediation in Express — concrete code fixes
Remediation focuses on minimizing the impact of exposed API keys and ensuring that signatures cannot be reused across contexts. Bind keys to specific operations, include nonce or timestamp, and enforce strict validation beyond HMAC correctness.
Use short-lived keys and include contextual data in the signed payload to prevent replay and scope escalation:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.SHARED_SECRET;
function signPayload(secret, payload) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
function buildClientRequest(secret, operation, data) {
const nonce = Math.random().toString(36).slice(2);
const timestamp = Date.now();
const payload = { operation, data, nonce, timestamp };
const signature = signPayload(secret, payload);
return { payload, signature };
}
function verifyHmac(req, res, next) {
const key = req.headers['x-api-key'];
const receivedPayload = req.body.payload;
const receivedSignature = req.headers['x-signature'];
if (!key || !receivedPayload || !receivedSignature) {
return res.status(400).json({ error: 'Missing fields' });
}
// Ensure the key is permitted for this operation (lookup from a secure store)
const allowed = checkKeyForOperation(key, receivedPayload.operation);
if (!allowed) {
return res.status(403).json({ error: 'Key not authorized for this operation' });
}
// Reject old timestamps to mitigate replay
const now = Date.now();
if (Math.abs(now - receivedPayload.timestamp) > 30000) {
return res.status(400).json({ error: 'Request expired' });
}
const expectedSignature = signPayload(SHARED_SECRET, receivedPayload);
if (!crypto.timingSafeEqual(Buffer.from(receivedSignature), Buffer.from(expectedSignature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
app.post('/transfer', verifyHmac, (req, res) => {
res.json({ status: 'ok' });
});
// Example helper; in production, use a secure, centralized mapping
function checkKeyForOperation(key, operation) {
const allowed = {
'key-a-123': ['transfer', 'balance'],
'key-b-456': ['balance'],
};
return allowed[key] && allowed[key].includes(operation);
}
app.listen(3000);
Additional defenses complement HMAC-specific fixes:
- Never expose long-term shared secrets in client-side code; use short-lived tokens derived from a secure vault when possible.
- Enforce rate limiting per API key to reduce the impact of leaked credentials.
- Bind keys to IP ranges or mTLS client certificates where feasible to reduce phishing success.
- Log and monitor invalid signature and key mismatch events to detect probing or phishing attempts.
These changes ensure that even if an API key is phished, the signature’s contextual binding and short validity window limit the attacker’s ability to make unauthorized calls through your Express endpoints.