HIGH data exposurestrapihmac signatures

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.timingSafeEqual to 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 IDNameSeverity
CWE-200Exposure of Sensitive Information HIGH
CWE-209Error Information Disclosure MEDIUM
CWE-213Exposure of Sensitive Information Due to Incompatible Policies HIGH
CWE-215Insertion of Sensitive Information Into Debugging Code MEDIUM
CWE-312Cleartext Storage of Sensitive Information HIGH
CWE-359Exposure of Private Personal Information (PII) HIGH
CWE-522Insufficiently Protected Credentials CRITICAL
CWE-532Insertion of Sensitive Information into Log File MEDIUM
CWE-538Insertion of Sensitive Information into Externally-Accessible File HIGH
CWE-540Inclusion of Sensitive Information in Source Code HIGH

Frequently Asked Questions

Why can HMAC signatures in Strapi still lead to data exposure?
If the signature does not cover all parts of the request (e.g., query parameter order, headers, or timestamp), or if validation is inconsistent across endpoints, an attacker can reuse or slightly modify a valid signed request to access sensitive data.
What is a key practice to prevent replay attacks when using HMAC signatures in Strapi?
Include a timestamp and reject requests with timestamps outside a short window, and use a nonce or short-lived store to ensure each signed request is used only once.