HIGH cache poisoningsailshmac signatures

Cache Poisoning in Sails with Hmac Signatures

Cache Poisoning in Sails with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker manipulates cached responses so that subsequent users receive malicious or incorrect data. In Sails.js applications that use Hmac Signatures to validate request integrity, a common misconfiguration can make caching behavior interact poorly with signature verification, leading to cache poisoning opportunities.

Hmac Signatures typically involve signing a canonical representation of a request—such as selected headers, the request path, and the body—so that downstream services or caches can verify the request has not been altered. If a Sails app uses Hmac Signatures only for selected routes (e.g., admin or write paths) but caches responses at a CDN or reverse proxy based on non-signature-aware keys, an attacker may be able to inject a poisoned request that gets cached under a shared key. For example, if the cache key includes only the URL and ignores variations introduced by headers covered by the Hmac algorithm, a signed request crafted by an attacker could result in a cached response that is later served to other users who did not present a valid signature.

Consider a scenario where a Sails API endpoint accepts an X-User-Role header that influences authorization but is not included in the cache key, while the Hmac signature covers that header. An authenticated attacker could send a signed request with an elevated role; if the caching layer treats the response as cacheable based solely on the URL, the elevated-response may be stored and subsequently served to users with lower privileges who access the same URL without a valid signature, effectively bypassing intended access controls. This violates the assumption that signature validation is required for sensitive operations. The risk is especially pronounced when responses contain user-specific or privilege-sensitive data and are cached with weak or overly broad keys.

Another vector involves query parameters that affect data retrieval but are omitted from cache normalization rules. If a Sails endpoint accepts an X-Tenant-ID header or query parameter that determines the data scope, and the Hmac signature includes that parameter, a tenant-specific poisoned entry could be stored under a shared key. Subsequent requests lacking a valid signature but matching the same normalized cache key may receive another tenant’s data. Because the cache operates outside the application’s runtime logic, the Hmac Signature validation that occurred during the original signed request is not reapplied on cache hits, enabling cross-tenant information exposure.

To identify such conditions, scans should correlate caching rules with which request attributes are included in Hmac canonicalization. If cached responses vary based on inputs covered by the Hmac algorithm but are served without re-validation, the surface for cache poisoning increases. This is a design and configuration issue rather than a flaw in Hmac itself; the signature ensures integrity for the initiating request but does not automatically protect cached representations unless the caching layer respects the same security boundaries.

Hmac Signatures-Specific Remediation in Sails — concrete code fixes

Remediation focuses on aligning cache keys with the attributes covered by the Hmac signature and ensuring that cached responses are only served when the signature and cache key context match. Below are concrete examples showing how to compute and verify an Hmac signature in Sails and how to incorporate relevant request attributes into cache normalization.

Example Hmac Signature generation and verification in Sails

Use a strong algorithm like sha256 and a server-side secret that is not exposed to clients. The canonical string should include the HTTP method, path, selected headers, and the body to prevent tampering.

// utils/hmac.js
const crypto = require('crypto');

function buildCanonicalString(method, path, headers, body) {
  const includedHeaders = ['x-tenant-id', 'x-user-role'];
  const headerParts = includedHeaders
    .map((key) => `${key.toLowerCase()}:${(headers[key] || '').toLowerCase()}`)
    .filter(Boolean);
  return [method.toUpperCase(), path, ...headerParts, body || ''].join('\n');
}

function generateHmac(secret, method, path, headers, body) {
  const canonical = buildCanonicalString(method, path, headers, body);
  return crypto.createHmac('sha256', secret).update(canonical).digest('hex');
}

function verifyHmac(secret, method, path, headers, body, receivedSignature) {
  const expected = generateHmac(secret, method, path, headers, body);
  // Use timing-safe compare
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
}

module.exports = { generateHmac, verifyHmac, buildCanonicalString };

In your Sails controller, verify the signature before processing the request:

// api/controllers/SecureController.js
const { verifyHmac, buildCanonicalString } = require('../../utils/hmac');

module.exports = {
  create: async function (req, res) {
    const secret = sails.config.custom.hmacSecret;
    const method = req.method;
    const path = req.path;
    const headers = req.headers;
    let body = '';

    // Collect body if present (for JSON payloads)
    if (req.is('application/json') && req._sails.hooks.req.body !== undefined) {
      body = JSON.stringify(req.body || {});
    }

    const receivedSignature = req.headers['x-hmac-signature'];
    if (!receivedSignature) {
      return res.unauthorized('Missing Hmac signature');
    }

    const canonical = buildCanonicalString(method, path, headers, body);
    const isValid = verifyHmac(secret, method, path, headers, body, receivedSignature);
    if (!isValid) {
      return res.forbidden('Invalid Hmac signature');
    }

    // Proceed with business logic
    return res.ok({ message: 'Request authenticated via Hmac' });
  }
};

Cache key normalization that aligns with Hmac coverage

Ensure that cache keys include all request attributes that are part of the Hmac canonical string. In a reverse proxy or application-level cache, define a normalization rule that omits or hashes only safe attributes. Below is an illustrative Node.js cache-key builder used before a cached response is stored or retrieved.

// utils/cacheKey.js
function buildCacheKey(req) {
  const includedHeaders = ['x-tenant-id', 'x-user-role'];
  const headerValues = includedHeaders
    .map((key) => `${key}:${(req.headers[key] || 'none').toLowerCase()}`)
    .filter(Boolean);
  return {
    key: `v1:${req.method.toLowerCase()}:${req.path}:${headerValues.join('|')}`,
    varyHeaders: includedHeaders
  };
}

// Example usage within a Sails hook or service
const { buildCacheKey } = require('./cacheKey');
const cacheEntry = buildCacheKey(req);
// Use cacheEntry.key to store/lookup cached responses

By tying cache keys to the same inputs covered by the Hmac signature, you ensure that a cached response cannot be reused across different security contexts. Additionally, avoid caching responses that include sensitive data unless the cache key incorporates tenant or role identifiers covered by the signature, and always prefer short TTLs for responses that vary by authorization context.

Frequently Asked Questions

How can I determine if my cache keys properly align with Hmac-covered attributes?
Compare the set of request attributes included in your Hmac canonical string (e.g., method, path, specific headers) with the attributes used to compute your cache key. If any attribute that can influence authorization or data scope is in the Hmac input but omitted from the cache key, you risk cache poisoning. Use logging or tracing to inspect cache hits and misses alongside the signature verification outcome.
Is it safe to cache responses that vary by Hmac-signed headers if I include those headers in the cache key?
Including Hmac-covered headers in the cache key reduces risk because responses are segregated by those header values. However, ensure that the header values used in the key are those that are also validated by your application logic and that the cache respects authorization boundaries. Avoid caching responses containing highly sensitive data unless necessary, and always combine cache key segregation with proper signature validation on each request.