MEDIUM clickjackingsailshmac signatures

Clickjacking in Sails with Hmac Signatures

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

Clickjacking is a client-side attack that tricks a user into interacting with a hidden or disguised element inside an embedded page. When Sails.js applications embed responses inside frames or iframes and rely solely on HMAC signatures for integrity without additional anti-clickjacking controls, the signature can indirectly support an exploitable flow. A typical scenario: an endpoint protected by HMAC signatures issues a JSON payload or form intended for use inside a specific origin. If that response is embeddable in an attacker-controlled page via an iframe, the user may be made to perform unintended actions (e.g., clicking a button that triggers state-changing requests). The HMAC signature on the payload or request ensures data integrity between the server and the client, but it does not prevent the resource from being embedded in a malicious context. Therefore, the combination creates a situation where integrity is preserved but authorization and UI confinement are not enforced, enabling clickjacking via embedded content that appears legitimate. Without frame restriction headers such as Content-Security-Policy frame-ancestors or X-Frame-Options, browsers will render the embedded resource, and the HMAC-verified UI elements can be overlaid with invisible controls to hijack user input.

Hmac Signatures-Specific Remediation in Sails — concrete code fixes

Remediation focuses on preventing embedding and ensuring that HMAC-signed flows are not usable in an unintended context. You should apply frame-protection headers and ensure HMAC verification is tied to context-bound tokens or one-time nonces to reduce risk. Below are concrete Sails.js examples.

Set frame-protection headers in Sails policies

// api/policies/csp-frame-policy.js
module.exports.cspFramePolicy = function (req, res, next) {
  // Prevent any site from framing this response
  res.set('Content-Security-Policy', "frame-ancestors 'none'");
  // For broader browser compatibility you can also send X-Frame-Options
  res.set('X-Frame-Options', 'DENY');
  return next();
};

Register the policy in config/policies.js to apply it to relevant controllers or actions:

// config/policies.js
module.exports.policies = {
  '*': ['cspFramePolicy'],
  'SensitiveController': {
    '*': ['cspFramePolicy', 'requireHmac']
  }
};

HMAC-signed request with nonce and origin binding in Sails

When issuing HMAC-signed tokens or responses, bind them to the request origin and a nonce to prevent reuse in embedded contexts. The following example shows how to generate and verify HMAC-signed payloads that include origin and nonce.

Generating a signed response (server-side):

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

function generateSignedPayload(user, origin, nonce, secret) {
  const data = {
    sub: user.id,
    origin: origin,
    nonce: nonce,
    exp: Math.floor(Date.now() / 1000) + 300 // 5 minutes
  };
  const payload = Buffer.from(JSON.stringify(data)).toString('base64url');
  const signature = crypto.createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return { payload, signature };
}

module.exports.generateSignedPayload = generateSignedPayload;

Using the signed payload in a Sails controller action:

// api/controllers/ActionController.js
const { generateSignedPayload } = require('../helpers/hmac');

module.exports.performAction = async function (req, res) {
  const origin = req.headers.origin || req.get('referer') || '';
  const nonce = req.session?.hmacNonce || cryptoRandomString({ length: 16, type: 'url' });
  const secret = sails.config.custom.hmacSecret;
  const { payload, signature } = generateSignedPayload(req.session.user, origin, nonce, secret);

  // Example: returning a form with HMAC-signed hidden fields for safe consumption
  return res.view('action', {
    payload,
    signature,
    frameProtection: true
  });
};

Verifying the signed payload (server-side):

// api/helpers/verifyHmac.js
const crypto = require('crypto');

function verifySignedPayload(payload, receivedSignature, secret, expectedOrigin) {
  const computedSignature = crypto.createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(receivedSignature))) {
    throw new Error('Invalid signature');
  }
  const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
  if (decoded.origin !== expectedOrigin) {
    throw new Error('Origin mismatch — possible embedding attempt');
  }
  if (Math.floor(Date.now() / 1000) > decoded.exp) {
    throw new Error('Token expired');
  }
  return decoded;
}

module.exports.verifySignedPayload = verifySignedPayload;

In the receiving endpoint, validate the origin against the one stored in the session or a strict allow-list, and reject requests that do not match. This prevents an attacker from forging a valid HMAC-signed request via an embedded iframe because they cannot know or reproduce the nonce and origin binding without access to the server secret.

Frequently Asked Questions

Does HMAC signing alone prevent clickjacking in Sails apps?
No. HMAC signatures ensure data integrity but do not stop a page from being embedded in an iframe. You must add frame-protection headers such as Content-Security-Policy frame-ancestors or X-Frame-Options to prevent clickjacking.
How can I safely use HMAC-signed payloads in Sails without exposing them to embedding?
Bind the HMAC to the request origin and a nonce, verify origin and expiration server-side, and enforce frame-ancestors 'none' (or a strict allow-list) via CSP and X-Frame-Options headers.