0

Why... Just Why...

Due to Shopify being unwilling to modify security headers further then removing them, the Cloudflare "Orange-to-Orange" problem (since Shopify uses Cloudflare as well), and my unwillingness to write a whole front end for the Shopify API just to avoid framing Shopify,

I need to prevent clickjacking without HTTP headers even when the website uses an authorized frame

Proposed Solution:

The solution is split into two parts, the top frame and the subframe. The pages are sent using https, so man-in-the-middle attacks are less likely unless the attacker has compromised the browser. The proposed solution relies on browser security. This means the content of the frames is unmodified, but the top frame might not exist or be replaced with (a) frame(s) from an attacker.

Top Frame Solution

The top frame needs to:

  1. Verify that it is the top frame
  2. Receive an identifying secret from the subframe
  3. Echo the secret assuring the subframe it is in good hands

To verify the frame is at the top:

function relocate() {
  top.location = 'my.website.com';
}
    
if (self === top) {
  // listen for message
} else {
  setInterval(relocate, 5000);
}

To receive an identifying secret and echo it:

function listener(event){ 
  if (event.origin !== 'https://my.other.website.com' ){
    return;
  }
  postMessage(event.data, 'https://my.other.website.com');
}

This should verify the identity of the top frame to the subframe since the browser is trusted to only send the secret message to the top frame if it has the correct domain. (Note that I opted for this method because the domain of the top frame is inaccessible from the subframe. Here is what developer.mozilla.org says about postMessage:

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

Normally, scripts on different pages are allowed to access each other if and only if the pages they originate from share the same protocol, port number, and host (also known as the "same-origin policy"). window.postMessage() provides a controlled mechanism to securely circumvent this restriction (if used properly).

Subframe Solution

The Subframe needs to:

  1. Ensure its script runs or scuttle the website
  2. Verify there is only a two windows, itself and a frame above it
  3. Verify the origin of the frame above it

OWASP provides a (cheat sheet)[https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#best-for-now-legacy-browser-frame-breaking-script] about clickjacking protection. I took the following scuttling style code from their solution for legacy browsers without clickjacking header support:

<style id="antiClickjack">
  body{display:none !important;}
</style>

Unfortunately the OWASP code was designed to prevent all framing. Instead the subframe needs to check that it is not the top frame, and there is only one frame above it:

if (self !== top && self.parent == top) {
 // continue sending origin verification
}

And finally including the above check to verify the top from origin:

var rnd = 0;
function sub_listener(event) {
  if (event.origin !== top_url || event.data != rnd){
     return;
  }

  var antiClickjack = document.getElementById('antiClickjack');
  antiClickjack.parentNode.removeChild(antiClickjack);
}

function verify() {
  if (self !== top && self.parent == top) {
    if (typeof crypto !== 'undefined') {
      rnd = crypto.getRandomValues(new Uint32Array(1))[0];
    } else {
      rnd = Math.random();
    }
    // set up sub_listener for the response
    postMessage(rnd, top_url);
  }
}

verify();

Top Frame Complete Solution

let sub_url = 'https://subframe.site';

function relocate() {
  top.location = sub_url;
}

// top frame only
function top_listener(event) {
  if (event.origin !== sub_url){
    return;
  }
  postMessage(event.data, sub_url);
}

if (self === top) {
  if (typeof addEventListener !== 'undefined'){ 
    addEventListener('message', top_listener, false);
  } else {
    attachEvent('onmessage', top_listener);
  }
} else {
  relocate()
  setInterval(relocate, 5000);
}

Subframe Complete Solution

function relocate() {
  top.location = top_url;
}

// subframe only
var countdown = setInterval(relocate, 10000);
var rnd = 0;
function sub_listener(event) {
  if (event.origin !== top_url || event.data != rnd){
     return;
  }

  if (countdown !== null) {
    clearInterval(countdown);
    countdown = null;
    var antiClickjack = document.getElementById('antiClickjack');
    antiClickjack.parentNode.removeChild(antiClickjack);
  }
}

function verify() {
  if (self !== top && self.parent == top) {
    if (typeof crypto !== 'undefined') {
      rnd = crypto.getRandomValues(new Uint32Array(1))[0];
    } else {
      rnd = Math.random();
    }
    postMessage(rnd, top_url);
    clearInterval(countdown);
  }
}

if (typeof addEventListener !== 'undefined'){ 
  addEventListener('message', sub_listener, false);
} else { 
  attachEvent('onmessage', sub_listener);
}

verify();

Question

Do any of you see obvious issues with the solution? What attack vectors could bypass this?

Thank you for reading it (or at least parts)! Feel free to ask me more about why I need to go this direction (I deserve it) but lets not be dogmatic about it.

Thank you for reading and commenting!

5
  • I'd highly recommend to reconsider using their API. Solving a server-side issue with a client-side java-script based solution is like settings a cookie admin: false in the browser to make sure the user is unable to use administrative functionalities.
    – Jeroen
    Commented Oct 27, 2021 at 17:01
  • The difference is that we are trusting the user and their browser in this case, since we are actually acting in their best interests. Users don't want to be spied on and have their credit cards / bank info stolen and browsers want to be secure for their users too.
    – Tyler
    Commented Oct 28, 2021 at 9:30
  • Even traditional HTTP headers rely upon the browser to enforce them. Clickjacking / frame busting protection is all about the client.
    – Tyler
    Commented Oct 28, 2021 at 9:31
  • 1
    What's your question?
    – schroeder
    Commented Oct 28, 2021 at 10:23
  • Thanks for the feedback, I've renamed the Discussion section to Question and made it more obvious. My question is: "Do any of you see obvious issues with the solution? What attack vectors could bypass this?"
    – Tyler
    Commented Oct 28, 2021 at 13:55

0

You must log in to answer this question.

Browse other questions tagged .