Formula Injection with Hmac Signatures
How Formula Injection Manifests in Hmac Signatures
Formula injection in the context of HMAC signatures occurs when untrusted input is incorporated into cryptographic operations without proper validation, potentially allowing attackers to manipulate signature generation or verification. This manifests in several critical ways:
// Vulnerable HMAC implementation
function createSignedUrl(url, secret) {
const params = new URLSearchParams(url.split('?')[1]);
const userId = params.get('user_id'); // Untrusted input
// Formula injection point: userId concatenated directly
const message = `url=${url}&user=${userId}`;
const hmac = crypto.createHmac('sha256', secret)
.update(message)
.digest('hex');
return `${url}&hmac=${hmac}`;
}
// Attacker crafts: ?user_id=1%26role=admin
// Resulting message: 'url=...&user=1&role=admin'
// This changes the signed message structure without detectionThe core issue is that formula injection allows attackers to manipulate the message composition formula used in HMAC operations. When user-controlled data is concatenated or combined without proper encoding, attackers can inject additional parameters or alter the message structure.
// Another vulnerable pattern
function verifyHmac(url, secret) {
const urlObj = new URL(url);
const hmac = urlObj.searchParams.get('hmac');
const userId = urlObj.searchParams.get('user_id'); // Untrusted
// Formula injection: userId directly used in message
const expected = crypto.createHmac('sha256', secret)
.update(`${urlObj.origin}${urlObj.pathname}?user_id=${userId}`)
.digest('hex');
return hmac === expected;
}
// Attacker crafts: ?user_id=1%0Aadmin=true
// The newline breaks the message structureFormula injection can also occur in token-based HMAC systems where claims are manipulated:
// JWT-style vulnerable implementation
function createToken(payload, secret) {
const header = { alg: 'HS256' };
const encodedHeader = base64url(JSON.stringify(header));
const encodedPayload = base64url(JSON.stringify(payload)); // Formula injection here
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
const signature = crypto.createHmac('sha256', secret)
.update(unsignedToken)
.digest('base64');
return `${unsignedToken}.${signature}`;
}
// Attacker manipulates payload structure to alter signature calculationTime-based HMAC signatures are particularly vulnerable when timestamp manipulation isn't properly validated:
// Vulnerable time-based HMAC
function createTimedSignature(data, secret, timestamp) {
// Formula injection: timestamp concatenated without validation
const message = `${data}.${timestamp}`;
return crypto.createHmac('sha256', secret)
.update(message)
.digest('hex');
}
// Attacker can manipulate timestamp to extend validity or cause replay attacksHMAC Signatures-Specific Detection
Detecting formula injection in HMAC implementations requires analyzing both the code structure and runtime behavior. Here are HMAC-specific detection methods:
Static Code Analysis
Look for these HMAC-specific patterns that indicate formula injection vulnerabilities:
import ast
def find_hmac_vulnerabilities(code):
"""Detect HMAC formula injection patterns"""
tree = ast.parse(code)
vulnerabilities = []
for node in ast.walk(tree):
# Look for crypto.createHmac or similar calls
if isinstance(node, ast.Call):
if (isinstance(node.func, ast.Attribute) and
node.func.attr == 'createHmac' and
any('crypto' in ast.unparse(n) for n in ast.walk(node.func))):
# Check if update() receives concatenated user input
for n in ast.walk(node):
if isinstance(n, ast.Call) and
isinstance(n.func, ast.Attribute) and
n.func.attr == 'update':
# Check if arguments contain string concatenation
if isinstance(n.args[0], ast.BinOp):
vulnerabilities.append({
'type': 'formula_injection',
'location': ast.get_source_segment(code, n),
'severity': 'high'
})
return vulnerabilities
Runtime Detection with middleBrick
middleBrick's black-box scanning approach specifically targets HMAC signature vulnerabilities through active testing:
| Test Type | HMAC-Specific Check | Detection Method |
|---|---|---|
| Input Validation | Parameter injection in HMAC messages | Attempts to inject special characters, newlines, and delimiters |
| Authentication Bypass | Signature manipulation | Modifies signed parameters to test verification logic |
| Data Exposure | Information leakage through HMAC failures | Analyzes error messages for sensitive data exposure |
middleBrick's scanner automatically tests HMAC implementations by:
- Submitting URLs with crafted parameters that attempt to break message composition formulas
- Testing timestamp manipulation in time-based HMAC signatures
- Analyzing response differences when signature verification fails
- Checking for inconsistent behavior between valid and invalid signatures
The scanner reports findings with severity levels and specific remediation guidance for HMAC implementations.
Manual Testing Methodology
Security testers can manually probe HMAC implementations using these techniques:
function testHmacFormulaInjection(endpoint, secret) {
// Test 1: Parameter injection
const url1 = `${endpoint}?user_id=1&role=admin×tamp=1234`;
const url2 = `${endpoint}?user_id=1%26role=admin×tamp=1234`;
// If both produce same signature, parameter injection works
// Test 2: Timestamp manipulation
const url3 = `${endpoint}?user_id=1×tamp=9999999999`;
// Test 3: Message structure manipulation
const url4 = `${endpoint}?user_id=1%0Aadmin=true`;
return { url1, url2, url3, url4 };
}
Look for inconsistencies in how different parameter encodings affect the final HMAC calculation.
HMAC Signatures-Specific Remediation
Remediating formula injection in HMAC signatures requires architectural changes to how messages are constructed and verified. Here are HMAC-specific fixes:
Canonical Message Construction
The most critical fix is implementing canonical message construction that eliminates formula injection:
// Secure HMAC implementation with canonicalization
function createSecureHmacSignature(params, secret) {
// Step 1: Validate and sanitize all inputs
const validatedParams = validateParams(params);
// Step 2: Create canonical string representation
const canonicalString = createCanonicalString(validatedParams);
// Step 3: Compute HMAC on canonical string
const hmac = crypto.createHmac('sha256', secret)
.update(canonicalString, 'utf8')
.digest('hex');
return hmac;
}
function createCanonicalString(params) {
// Sort parameters by key for deterministic ordering
const sortedKeys = Object.keys(params).sort();
// Percent-encode keys and values
return sortedKeys.map(key => {
const encodedKey = encodeURIComponent(key);
const encodedValue = encodeURIComponent(params[key]);
return `${encodedKey}=${encodedValue}`;
}).join('&');
}
// Example:
// Input: { user_id: '1', role: 'admin' }
// Output: 'role=admin&user_id=1'
Input Validation and Sanitization
Implement strict validation before HMAC processing:
const HMAC_PARAM_SCHEMA = {
user_id: { type: 'string', pattern: '^[0-9]+$' },
role: { type: 'string', enum: ['user', 'admin'] },
timestamp: { type: 'integer', min: Date.now() - 86400000 }
};
function validateParams(params) {
const validated = {};
for (const [key, schema] of Object.entries(HMAC_PARAM_SCHEMA)) {
if (!(key in params)) {
throw new Error(`Missing required parameter: ${key}`);
}
const value = params[key];
// Type validation
if (schema.type === 'string' && typeof value !== 'string') {
throw new Error(`Invalid type for ${key}: expected string`);
}
if (schema.type === 'integer' && !Number.isInteger(Number(value))) {
throw new Error(`Invalid type for ${key}: expected integer`);
}
// Pattern validation
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
throw new Error(`Invalid format for ${key}`);
}
// Enum validation
if (schema.enum && !schema.enum.includes(value)) {
throw new Error(`Invalid value for ${key}: ${value}`);
}
// Range validation
if (schema.min && Number(value) < schema.min) {
throw new Error(`Value for ${key} too small`);
}
validated[key] = value;
}
return validated;
}
HMAC Library Best Practices
Use well-vetted HMAC libraries and follow these patterns:
// Node.js crypto module best practices
const crypto = require('crypto');
class SecureHmacSigner {
constructor(secret) {
if (!secret || secret.length < 32) {
throw new Error('HMAC secret must be at least 32 bytes');
}
this.secret = secret;
}
sign(params) {
const validated = validateParams(params);
const canonical = createCanonicalString(validated);
return crypto.createHmac('sha256', this.secret)
.update(canonical, 'utf8')
.digest('base64');
}
verify(params, signature) {
const expected = this.sign(params);
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
}
// Timing-safe comparison to prevent timing attacks
function timingSafeEqual(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
Testing Your Remediation
After implementing fixes, verify with these tests:
function testFormulaInjectionProtection() {
const secret = 'secure-random-secret-32-bytes';
const signer = new SecureHmacSigner(secret);
// Test 1: Normal case
const params1 = { user_id: '1', role: 'user' };
const sig1 = signer.sign(params1);
// Test 2: Parameter injection attempt
const params2 = { user_id: '1%26role=admin' };
const sig2 = signer.sign(params2);
// Test 3: Timestamp manipulation
const params3 = { user_id: '1', role: 'user', timestamp: '0' };
const sig3 = signer.sign(params3);
console.log('All signatures should be different:',
sig1 !== sig2 && sig1 !== sig3 && sig2 !== sig3);
// Test verification
console.log('Verification should work:', signer.verify(params1, sig1));
console.log('Verification should fail on tampered params:',
!signer.verify({ user_id: '2', role: 'user' }, sig1));
}