Data Exposure in Strapi with Hmac Signatures
Data Exposure in Strapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Strapi is a headless CMS that can expose data when HMAC signatures are used incorrectly or inconsistently between the API producer and consumer. A HMAC signature is typically generated with a shared secret and request components such as HTTP method, path, query parameters, and timestamp. If the server-side signature verification logic is incomplete, mismatched, or applied only to a subset of endpoints, an unauthenticated or low-privilege attacker may be able to reuse a valid signature to access sensitive data they should not see.
Consider an endpoint that returns user profile information and relies on a timestamp and a shared secret to sign the request. If Strapi does not enforce strict canonicalization of the signed string (for example, differing ordering of query parameters, inclusion or exclusion of certain headers, or inconsistent timestamp windows), an attacker can slightly alter the request and attempt a signature mismatch attack. Data exposure can occur when the API returns personal or sensitive fields such as email, role, or internal identifiers in responses that should be restricted.
In a black-box scan, middleBrick tests HMAC-related behavior by sending requests with modified query parameters, altered timestamps, and reused signatures to detect whether the server fails to reject tampered or replayed signed requests. If Strapi accepts a modified query string with a valid HMAC, this indicates that signature validation does not cover all components of the request, leading to potential data exposure. Another common pattern is when HMAC is used only for selected routes (e.g., admin endpoints) but omitted for others, creating an inconsistent security boundary where sensitive data can be retrieved through the unprotected path.
Real-world examples include endpoints that return sensitive configuration or logs when a valid HMAC is provided but the signature does not bind to the full request context. For instance, an attacker might change the resource ID in a query parameter while keeping the same signature, and if Strapi does not validate ownership or scope, confidential data can be leaked. This becomes more critical when responses include PII, financial details, or internal references that should remain hidden from unauthorized consumers.
To identify this class of issues, middleBrick compares signed requests with modified parameters and checks whether the server accepts them. The scanner also cross-references the OpenAPI specification to verify whether HMAC requirements are documented consistently across operations. If the spec defines security schemes based on HMAC but implementation does not enforce them uniformly, the discrepancy itself is a finding that can lead to data exposure.
Hmac Signatures-Specific Remediation in Strapi — concrete code fixes
Remediation focuses on ensuring HMAC signatures cover all relevant parts of the request and are validated consistently across endpoints. The canonical string used for signing should include the HTTP method, path, sorted query parameters, selected headers, and an appropriate timestamp nonce to prevent replay attacks. Below are concrete examples for Strapi that demonstrate a robust approach.
Example 1: Generating a HMAC signature on the client
const crypto = require('crypto');
function buildHmacSignature({ method, path, query, headers, secret, timestamp }) {
const sortedKeys = Object.keys(query).sort();
const canonicalQuery = sortedKeys.map(k => `${k}=${encodeURIComponent(query[k])}`).join('&');
const headersToSign = ['content-type'];
const canonicalHeaders = headersToSign.map(h => `${h}:${headers[h]}`).join(';');
const stringToSign = [method.toUpperCase(), path, canonicalQuery, canonicalHeaders, timestamp].join('\n');
return crypto.createHmac('sha256', secret).update(stringToSign).digest('hex');
}
const signature = buildHmacSignature({
method: 'GET',
path: '/api/users/me',
query: { timestamp: '1700000000000' },
headers: { 'content-type': 'application/json' },
secret: process.env.HMAC_SECRET,
timestamp: '1700000000000'
});
console.log(signature);
Example 2: Verifying the signature in Strapi middleware
const crypto = require('crypto');
module.exports = (config) => {
return async (ctx, next) => {
const timestamp = ctx.request.query.timestamp;
const receivedSignature = ctx.request.header['x-signature'];
const method = ctx.method;
const path = ctx.path;
const query = ctx.query;
const headers = { 'content-type': ctx.request.header['content-type'] || 'application/json' };
const secret = process.env.HMAC_SECRET;
if (!timestamp || Math.abs(Date.now() - Number(timestamp)) > 300000) {
ctx.status = 401;
ctx.body = { error: 'Invalid or expired timestamp' };
return;
}
const sortedKeys = Object.keys(query).sort();
const canonicalQuery = sortedKeys.map(k => `${k}=${encodeURIComponent(query[k])}`).join('&');
const canonicalHeaders = ['content-type'].map(h => `${h}:${headers[h]}`).join(';');
const stringToSign = [method.toUpperCase(), path, canonicalQuery, canonicalHeaders, timestamp].join('\n');
const expectedSignature = crypto.createHmac('sha256', secret).update(stringToSign).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(receivedSignature))) {
ctx.status = 401;
ctx.body = { error: 'Invalid signature' };
return;
}
await next();
};
};
Example 3: Ensuring canonicalization and replay protection
// In Strapi controller or service
const crypto = require('crypto');
function verifyRequest(ctx) {
const timestamp = ctx.request.query.timestamp;
const receivedSignature = ctx.request.header['x-signature'];
const nonceStore = ctx.app.store || new Set(); // external cache in production
if (typeof timestamp !== 'string' || isNaN(Number(timestamp))) {
throw new Error('Missing or invalid timestamp');
}
if (Math.abs(Date.now() - Number(timestamp)) > 300000) {
throw new Error('Request expired');
}
if (nonceStore.has(`${ctx.method}:${ctx.path}:${timestamp}`)) {
throw new Error('Replay detected');
}
const query = Object.keys(ctx.query)
.filter(k => k !== 'signature')
.sort()
.reduce((acc, k) => { acc[k] = ctx.query[k]; return acc; }, {});
const sortedKeys = Object.keys(query).sort();
const canonicalQuery = sortedKeys.map(k => `${k}=${encodeURIComponent(query[k])}`).join('&');
const headers = ['content-type'].map(h => `${h}:${ctx.request.header[h] || ''}`).join(';');
const stringToSign = [ctx.method.toUpperCase(), ctx.path, canonicalQuery, headers, timestamp].join('\n');
const expectedSignature = crypto.createHmac('sha256', process.env.HMAC_SECRET).update(stringToSign).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(receivedSignature))) {
throw new Error('Invalid signature');
}
nonceStore.add(`${ctx.method}:${ctx.path}:${timestamp}`);
setTimeout(() => nonceStore.delete(`${ctx.method}:${ctx.path}:${timestamp}`), 300000);
}
Remediation checklist
- Include HTTP method, path, sorted query parameters, selected headers, and a timestamp in the signed string.
- Use
crypto.timingSafeEqualto compare signatures to avoid timing attacks. - Reject requests with timestamps outside an acceptable window (for example, 5 minutes).
- Use a nonce or short-lived cache to prevent replay attacks.
- Apply the same HMAC validation logic consistently across all endpoints that handle sensitive data.
- Document the signature scheme in the OpenAPI spec so that consumers can generate valid signatures.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |