Crlf Injection in Express with Hmac Signatures
Crlf Injection in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject CRLF sequences (\r\n) into a header or header-like value. In Express, if user-controlled data is reflected in HTTP response headers and the application computes an Hmac signature over a header value that includes injected sequences, the signature can be abused to break integrity assumptions or to smuggle headers.
Consider an Express route that copies a query parameter into a custom header and signs that header with Hmac:
const crypto = require('crypto');
const express = require('express');
const app = express();
const SHARED_SECRET = 'super-secret';
app.get('/widget', (req, res) => {
const token = req.query.token; // user-controlled
const headerValue = token || 'default';
const signature = crypto.createHmac('sha256', SHARED_SECRET).update(headerValue).digest('hex');
res.set('X-Data', headerValue);
res.set('X-Signature', signature);
res.json({ received: headerValue });
});
app.listen(3000);
If an attacker supplies token=foo\r\nContent-Type: text/html, the header reflected becomes foo\r\nContent-Type: text/html. The Hmac is computed over the full string including the injected CRLF, so the server’s signature remains valid for that exact value. However, when the server sets res.set('X-Data', headerValue), many HTTP libraries treat the first CRLF as a header/body separator, causing Content-Type: text/html to be interpreted as a new response header. This header smuggling bypasses intended validation and can change how downstream caches or clients process the response.
In practice, this pattern is risky because the Hmac signature does not protect against injection within the value itself; it only ensures the value hasn’t been altered after signing. An attacker can exploit the injected CRLF to smuggle headers even when a signature is present, because the signature binds to the tainted value rather than to a safe canonical form. This can lead to cache poisoning, cross-user header injection, or bypass of security headers applied by middleware.
Another scenario involves using Hmac-signed headers for authentication or versioning (e.g., X-Api-Key and X-Request-Id). If any of those signed header values reflect unchecked user input and contain CRLF, an attacker can inject additional headers that the server will process, potentially elevating privileges or exfiltrating data depending on how downstream services interpret the smuggled headers.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
Remediation focuses on preventing CRLF characters from reaching header values and canonicalizing data before signing. Do not rely on the Hmac to detect injection; validate and sanitize inputs first.
- Strip or reject CRLF in user input used for headers:
const crypto = require('crypto');
const express = require('express');
const app = express();
const SHARED_SECRET = 'super-secret';
function sanitizeHeaderValue(value) {
if (typeof value !== 'string') return value;
// Remove carriage return and line feed to prevent header smuggling
return value.replace(/[\r\n]/g, '');
}
app.get('/widget', (req, res) => {
const token = req.query.token; // user-controlled
const headerValue = sanitizeHeaderValue(token) || 'default';
const signature = crypto.createHmac('sha256', SHARED_SECRET).update(headerValue).digest('hex');
res.set('X-Data', headerValue);
res.set('X-Signature', signature);
res.json({ received: headerValue });
});
app.listen(3000);
- Sign a canonical representation rather than the raw reflected value when possible. For example, sign a normalized token ID instead of the raw token string that may contain extra metadata:
const crypto = require('crypto');
const express = require('express');
const app = express();
const SHARED_SECRET = 'super-secret';
function normalizeToken(input) {
// Extract a clean token ID, rejecting unexpected formats
const match = String(input || '').match(/^[a-zA-Z0-9_-]{1,64}$/);
return match ? match[0] : 'default';
}
app.get('/widget', (req, res) => {
const raw = req.query.token;
const normalized = normalizeToken(raw);
const signature = crypto.createHmac('sha256', SHARED_SECRET).update(normalized).digest('hex');
res.set('X-Data', normalized);
res.set('X-Signature', signature);
res.json({ token: normalized, sig: signature });
});
app.listen(3000);
- When using Hmac-signed headers for authentication, validate the signature over a canonical set of header names and values, and enforce strict content constraints (e.g., no newlines, length limits, allowed character sets). Avoid including multiple user-supplied values in the same signed string where one can be manipulated to inject CRLF.
const crypto = require('crypto');
const express = require('express');
const app = express();
const SHARED_SECRET = 'super-secret';
function canonicalHeaderValue(token, requestId) {
// strict format reduces injection surface
const safeToken = typeof token === 'string' && /^[\w-]{1,128}$/.test(token) ? token : 'anon';
const safeId = typeof requestId === 'string' && /^[\w-]{1,64}$/.test(requestId) ? requestId : 'n/a';
return `${safeToken}|${safeId}`;
}
app.get('/resource', (req, res) => {
const token = req.query.token;
const requestId = req.query.id;
const payload = canonicalHeaderValue(token, requestId);
const signature = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
res.set('X-Payload', payload);
res.set('X-Signature', signature);
res.json({ payload, signature });
});
app.listen(3000);
These steps ensure the data used in headers and signatures is free of CRLF and that the signature covers a predictable, safe representation, reducing the risk of header smuggling even when attacker-controlled input is reflected.