Insecure Direct Object Reference with Hmac Signatures
How Insecure Direct Object Reference Manifests in Hmac Signatures
HMAC signatures are designed to authenticate and verify the integrity of API requests, but they can inadvertently enable Insecure Direct Object Reference (IDOR) vulnerabilities when improperly implemented. The core issue arises when HMAC-signed requests contain object identifiers that are predictable or sequentially generated, allowing attackers to modify these identifiers and access unauthorized resources.
A common HMAC IDOR pattern occurs in REST APIs where resource IDs are embedded in signed URLs or headers. Consider a banking API that signs transaction requests:
const hmac = crypto.createHmac('sha256', secretKey);
hmac.update('transaction/' + transactionId);
const signature = hmac.digest('hex');
An attacker who observes a legitimate request for transaction/1001 can simply increment to transaction/1002 and modify the HMAC signature accordingly. Since the signature only validates the message format and not the user's authorization to access that specific resource, the request will be accepted by the server.
This vulnerability becomes more severe when HMAC signatures are used to sign entire query parameters or JSON bodies containing object references. For example:
// Vulnerable pattern - signing entire payload
const payload = JSON.stringify({
userId: 123,
documentId: 456
});
const signature = crypto.createHmac('sha256', secretKey)
.update(payload)
.digest('hex');
// Attacker modifies documentId to 457 and recalculates signature
The fundamental problem is that HMAC provides message authentication but not authorization. The signature guarantees that the message hasn't been tampered with, but it doesn't verify whether the requester has permission to access the specific resource referenced in the message.
Another HMAC IDOR variant involves predictable nonce or timestamp values. When APIs use sequential nonces or timestamps as part of the signed message, attackers can guess valid values and craft requests for unauthorized resources:
// Predictable nonce pattern
const nonce = Date.now(); // Easily guessable
const message = 'resource/' + resourceId + '/' + nonce;
const signature = crypto.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
The server validates the signature and nonce but fails to check if the user owns or has access to the requested resource.
HMAC Signatures-Specific Detection
Detecting HMAC-based IDOR vulnerabilities requires both static analysis and dynamic testing approaches. The most effective detection combines automated scanning with manual review of HMAC implementation patterns.
Static analysis should focus on identifying HMAC signing code that incorporates predictable identifiers. Look for patterns where HMAC signatures are created using:
// Vulnerable patterns to flag
hmac.update(userId);
hmac.update(resourceId);
hmac.update(transactionId);
hmac.update(timestamp);
Tools like middleBrick can automatically scan HMAC implementations by analyzing the API's attack surface. The scanner examines request patterns, identifies HMAC signature usage, and tests for IDOR vulnerabilities by systematically modifying signed object identifiers and verifying if unauthorized access is granted.
Dynamic testing for HMAC IDOR involves creating test requests with modified object identifiers while maintaining valid signatures. This requires understanding the HMAC algorithm and key usage. A basic test script might look like:
async function testHmacIdor(baseUrl, originalRequest) {
// Extract HMAC parameters from original request
const { url, headers, body } = originalRequest;
const originalSignature = headers['x-hmac-signature'];
// Parse the signed message to identify object identifiers
const urlParts = url.split('/');
const resourceId = parseInt(urlParts[urlParts.length - 1]);
// Test adjacent resource IDs
for (let offset = 1; offset <= 10; offset++) {
const testId = resourceId + offset;
const testUrl = url.replace(resourceId.toString(), testId.toString());
// Create new request with modified ID
const testRequest = { ...originalRequest, url: testUrl };
// Send test request and check response
const response = await fetch(testUrl, {
method: 'GET',
headers: headers
});
if (response.status === 200) {
console.log(`IDOR found: accessed resource ${testId}`);
}
}
}
middleBrick's black-box scanning approach is particularly effective for HMAC IDOR detection because it doesn't require source code access. The scanner sends modified requests to the API endpoint, observes responses, and identifies when unauthorized resource access is granted despite valid HMAC signatures.
During scanning, middleBrick tests 12 security categories including BOLA/IDOR, attempting to access resources by modifying signed identifiers. The scanner maintains a database of observed HMAC patterns and systematically tests variations to identify authorization bypasses.
HMAC Signatures-Specific Remediation
Remediating HMAC-based IDOR vulnerabilities requires implementing proper authorization checks alongside signature validation. The most effective approach combines HMAC authentication with resource-level authorization verification.
The fundamental fix is to validate that the user has permission to access the specific resource identified in the HMAC-signed request. This requires implementing authorization logic that runs after HMAC verification:
async function handleRequest(req, res) {
// Step 1: Verify HMAC signature
const isValidSignature = verifyHmacSignature(req);
if (!isValidSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Step 2: Extract resource identifiers from request
const resourceId = extractResourceId(req);
const userId = extractUserId(req);
// Step 3: Authorize resource access
const hasAccess = await authorizeResourceAccess(userId, resourceId);
if (!hasAccess) {
return res.status(403).json({ error: 'Access denied' });
}
// Step 4: Process request
return res.json(await processRequest(req));
}
async function authorizeResourceAccess(userId, resourceId) {
// Check if user owns or has access to the resource
const resource = await db.collection('resources')
.findOne({ _id: resourceId, ownerId: userId });
return resource !== null;
}
For APIs that use HMAC to sign entire payloads containing multiple object references, implement granular authorization checks for each referenced resource:
async function processSignedRequest(body, signature) {
// Verify HMAC signature first
if (!verifyHmac(body, signature)) {
throw new Error('Invalid signature');
}
// Extract and validate all object references
const { userId, documentIds } = body;
// Check user authorization
if (!await userExists(userId)) {
throw new Error('Invalid user');
}
// Validate each document access
for (const docId of documentIds) {
if (!await canAccessDocument(userId, docId)) {
throw new Error(`Access denied to document ${docId}`);
}
}
// Process authorized request
return await processDocuments(documentIds);
}
Another effective remediation strategy is to include user-specific context in the HMAC message itself. This prevents attackers from simply modifying resource IDs and recalculating signatures:
// Include user ID and resource ownership in signed message
function createSecureHmac(userId, resourceId, secretKey) {
// Verify resource ownership before signing
if (!resourceBelongsToUser(userId, resourceId)) {
throw new Error('Resource does not belong to user');
}
const message = `${userId}:${resourceId}:${Date.now()}`;
const signature = crypto.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
return { message, signature };
}
// Server-side verification includes ownership check
function verifySecureHmac(message, signature, secretKey) {
const [userId, resourceId] = message.split(':');
// Verify both signature and resource ownership
const isValidSignature = crypto.createHmac('sha256', secretKey)
.update(message)
.digest('hex') === signature;
const isAuthorized = resourceBelongsToUser(userId, resourceId);
return isValidSignature && isAuthorized;
}
For APIs using predictable nonces or timestamps in HMAC signatures, implement nonce validation that checks for sequential patterns and limits the valid range:
function validateNonce(nonce, userId) {
const now = Date.now();
const nonceTime = parseInt(nonce);
// Check if nonce is within acceptable time window
const isRecent = Math.abs(now - nonceTime) < 5 * 60 * 1000; // 5 minutes
// Check if user has already used this nonce
const isDuplicate = nonceCache.has(`${userId}:${nonce}`);
if (!isRecent || isDuplicate) {
return false;
}
// Store nonce to prevent reuse
nonceCache.set(`${userId}:${nonce}`, true);
setTimeout(() => nonceCache.delete(`${userId}:${nonce}`), 5 * 60 * 1000);
return true;
}
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |