Type Confusion with Hmac Signatures
How Type Confusion Manifests in Hmac Signatures
HMAC (Hash-based Message Authentication Code) signatures are widely used in APIs to verify the integrity and authenticity of requests. A typical pattern involves the client computing an HMAC over a set of request parameters (e.g., method, path, query parameters, body) using a shared secret, then sending the signature in a header (like X-Signature). The server recomputes the HMAC over the same parameters and compares it to the provided signature. If they match, the request is considered legitimate.
Type confusion in this context arises when the server and client (or an attacker) disagree on the data type or serialization format of the signed parameters. Because HMAC operates on bytes, any inconsistency in how parameters are converted to bytes will produce a different HMAC, potentially allowing an attacker to bypass signature validation.
Common attack patterns include:
- Numeric vs. String Representation: If a parameter like
user_idis sent as a number (123) but the server expects a string ("123"), the byte representation differs. An attacker might exploit this by sending the parameter in a different type and adjusting the signature accordingly, if the server's HMAC computation does not normalize types. - Boolean and Null Values: JSON distinguishes between
true,false, andnull. If the server treatsnullas an empty string or ignores it, while the client includes it, the HMAC will diverge. - Encoding Issues: If the server uses a platform-specific default encoding (e.g.,
utf8in Node.js vs.ISO-8859-1in Java) when converting strings to bytes, the same string can produce different HMACs. - Serialization Format: The order and formatting of parameters matter. For example, concatenating parameters as
key1=value1&key2=value2vs. using JSON with sorted keys. If an attacker can inject a parameter that changes the serialization order (e.g., by adding a duplicate key), they might control the signed string.
Here is a vulnerable Node.js example that computes an HMAC by simply concatenating parameter values without type normalization:
const crypto = require('crypto');
function computeSignature(params, secret) {
// Vulnerable: direct concatenation of values without type handling
const data = Object.values(params).join('');
return crypto.createHmac('sha256', secret).update(data).digest('hex');
}
// Attacker-controlled params: user_id as number vs. string
const params1 = { user_id: 123, amount: 100 };
const params2 = { user_id: '123', amount: 100 };
console.log(computeSignature(params1, 'secret')); // Different from params2
console.log(computeSignature(params2, 'secret'));If the server uses the first representation but the client (or attacker) uses the second, the HMAC validation will fail for legitimate requests or be bypassed by an attacker who crafts both the parameters and the signature.
Hmac Signatures-Specific Detection
Detecting type confusion in HMAC signatures requires testing how the API handles parameter types during signature verification. Manual testing involves sending the same logical parameter in different data types (e.g., integer, string, boolean) and observing whether the server accepts the request with a valid signature computed for one type when the parameter is sent in another type.
For example, you can:
- Capture a legitimate request with a known signature.
- Replay the request but change a numeric parameter to a string (e.g.,
user_id=123touser_id="123"in JSON) and recompute the signature accordingly. - If the server accepts the request, it indicates that the server's HMAC computation does not normalize types, leading to a vulnerability.
How middleBrick helps: middleBrick's black-box scanning includes an Authentication check that tests for inconsistencies in HMAC validation. It automatically:
- Identifies if an API uses HMAC signatures (by looking for headers like
X-Signature,Authorization: HMAC, etc.). - Attempts to compute signatures for variations of the same request with parameters expressed in different types (numbers as strings, booleans as integers, etc.).
- Compares server responses to detect if the signature validation is bypassed or if the server returns different error messages for different type representations.
middleBrick also checks for Input Validation issues that could exacerbate type confusion, such as accepting duplicate parameters or non-canonical JSON. The scan output will flag any parameter type inconsistencies as a finding under the Authentication category with a severity rating and remediation guidance.
Hmac Signatures-Specific Remediation
The fix is to ensure that both client and server use a canonical, type-stable serialization for the signed data. This means:
- Define a strict data model for all signed parameters (e.g.,
user_idmust be an integer,activea boolean). - Serialize parameters in a deterministic way: use a standard format like JSON with sorted keys and no whitespace, or a well-defined string concatenation scheme with explicit type indicators.
- Normalize types before serialization: convert all values to their canonical string representation (e.g., numbers to strings without leading zeros, booleans to
"true"/"false"). - Always use a fixed character encoding (UTF-8) when converting the serialized string to bytes for HMAC computation.
Here is a corrected Node.js example using canonical JSON serialization with sorted keys and UTF-8 encoding:
const crypto = require('crypto');
function canonicalize(params) {
// Sort keys and stringify without whitespace
const sortedKeys = Object.keys(params).sort();
const sortedParams = {};
for (const key of sortedKeys) {
// Normalize values to consistent types if needed
// For example, ensure numbers are represented as numbers in JSON
sortedParams[key] = params[key];
}
return JSON.stringify(sortedParams);
}
function computeSignature(params, secret) {
const data = canonicalize(params);
// Explicitly use UTF-8 encoding
return crypto.createHmac('sha256', secret).update(data, 'utf8').digest('hex');
}
// Now both params1 and params2 produce the same signature if user_id is a number
const params1 = { user_id: 123, amount: 100 };
const params2 = { user_id: 123, amount: 100 }; // same type
console.log(computeSignature(params1, 'secret'));
console.log(computeSignature(params2, 'secret')); // identicalIf the API must accept parameters in multiple formats (e.g., query string vs. JSON body), the server should first parse them into a canonical internal representation before computing the HMAC. Additionally, always validate that the received parameters match the expected types after parsing, and reject requests with type mismatches.
For existing APIs, a migration strategy is to version the signing scheme (e.g., X-Signature-Version: 2) and gradually roll out the canonical serialization while keeping the old version for backward compatibility until all clients are updated.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |