Konubinix' opinionated web of thoughts

Sharing Data From an Iframe

Fleeting

introduction

using an iframe is a practical way to run some third party code in a web page. The iframe and the toplevel frame can communicate using postMessage, but both are not able to mess with the other.

first party

It may happen though, that you want to run some code in an iframe from the same domain. In that case, the toplevel and the iframe share a lot of stuff, like the localstorage, the cookies etc.

<button onclick="localStorage.setItem('foobar', 'toplevel')">Set foobar to toplevel</button>

<button onclick="alert(localStorage.getItem('foobar'))">Show foobar</button>

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      body {
      background-color: cyan;
      }
    </style>
  </head>
  <body>
    <button onclick="localStorage.setItem('foobar', 'iframe')">Set foobar to iframe</button>
    <button onclick="alert(localStorage.getItem('foobar'))">Show foobar</button>
  </body>
  <script>window.addEventListener("message", (event) => {if(event.origin === window.location.origin){ localStorage.setItem('foobar', event.data.foobar)}})</script>
</html>

In that case, because the iframe runs in the same domain as the toplevel window, it is said to be first party. It shares its data easily.

Try opening several tabs of this page and see how changing the content in one will impact all of them.

third party

Now takes the source code of this page and start a local web server

python -m http.server

Read this page from http://localhost:8000. In that case, the iframe is third party, therefore, it won’t share its data with the toplevel frame.

By clicking on the buttons, you will see that there is no way the toplevel window can show “iframe” when clicking on the “Show foobar”

See postMessage() and iframes to see how both can still communicate to share data.

third party vs first party

Now, keep the localhost server and open this konubinix.eu page elsewhere.

Now, you can change the value of foobar in the iframe and see its value in the konubinix.eu page, because this frame and the toplevel frame of the other page share the same local storage.

Wait, it does not work?

Indeed, if your web browser is recent enough, you might realize that you cannot share the data between the iframe in konubinix.eu to the toplevel frame in the other tab.

This is because the browsers try hard not leaking information about you without control.

It appears that the data isolation takes into account the whole hierarchy of frames when in the third party scenario.

the login use case

Now, consider that you provide an iframe that has to get access to some services. You can either.

  1. let the login occur in the toplevel frame,
  2. let the login occur in the iframe, not hidden,
  3. do the login in a separate window and then communicate the authorization tokens to the iframe.

Doing 1, You would lose the interest of isolation, whatever services accessible to the iframe will also be accessible to the toplevel window,

It looks like big Identity Providers like google won’t allow 2.

Therefore, only 3 remains. As seen earlier, on old browsers, simply sharing the localstorage is enough, but it is becoming less and less so.

How could we share the authorization from the opened web page to the iframe thenĀ ?

access storage api

This was done exactly for this

legitimate uses for third-party cookies and unpartitioned state that we still want to enable, even with these default restrictions in place. Examples include single sign-on (SSO) with federated identity providers (IdPs), or persisting user details such as location data or viewing preferences across different sites.

https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API

But as of today ([2025-01-22 Wed]) its support is not ideal.

Also, using access storage api should theoretically trigger a popup asking the user to validate the data sharing. That is not ideal UX wise.

use a separate web page instead of an iframe

The difficulty with sharing data is associated to that third party vs first party stuff, so one could try to remove the problem altogether and use a separate tab rather than an iframe.

But that comes with more difficulties. Two of the features of iframe are lost:

  1. run at the same time as the top level page,
  2. can easily share data with postmessage,

To implement a separate page would mean ask the end user to keep both open at the same time or provide a message broker to replay data from one to the other. Also, this communication would need some cryptography in place to be secure.

So far, I believe that only gundb would provide all the features to implement this, but that does not seem to be obvious.

use the window.opener

This feature has been there for a long time (opera supports it since 1998). It requires an explicit interaction from the user and that the toplevel page does not use blocking features such as “noopener” or “target=_blank”.

The toplevel page initiates the login process using the window.open method.

<button onclick="window.open('https://konubinix.eu/ipfs/bafkreigvjud5tqny5ybzhnhy5xiubwlk3qr3rndjspg6ecpvw3s3hef6rq')">Open login window</button>

The opened page supposedly deal with the login, involving several page changes.

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
     <div>This is the first page of the simulated authorization scheme</div>
     <a href="https://konubinix.eu/ipfs/bafkreiegzdwpme47ofy6rww3gqoyod4uu7epdbavdbchokqhpvalrlk3u4">click me to get to the second page</a>
     <div>you may navigate to google and go back to see how this makes the opener unavailable and break our design</div>
     <a href="https://google.com">go to google</a>
    <div>or try using a self hosted site doing this</div>
    <a href="http://localhost:8001">go to a self hosted site doing the coop=same-origin</a>
  </body>
</html>

The second page simulates the fact that we ended the flow and are ready to send the authorization data to the iframe before quitting.

Note that here we use postMessage. This works because in the code of the iframe above we captured that message.

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
     <div>This is the second page of the simulated authorization scheme. It simulates the end of the login, that deals with the authorization and quits.</div>
     <div>If you went to a site setting coop to same-origin in the previous step, this will fail (you will see the alert showing the error).</div>
     <button onclick="
      try {
         window.opener.frames[0].postMessage({'foobar': 'popup'});
      } catch (e) {
         alert(e);
      }
      window.close()">click me to send some data to the iframe and quit</button>
  </body>
</html>

Now, load this page in the third party scenario, using python -m http.server, then click on this link.

Then, go through both pages, until the popup is closed.

Then, go back to the beginning of this document, where the iframe is displayed.

Click on “Show foobar” and you will see “popup”, indicating that we successfully sent the data from the popup.

I don’t know how much time this opener method will work, but is it quite convenient, waiting for the Storage Access API to grow more mature.

Note that this design works as long as you don’t need to navigate to a site setting the Cross-Origin-Opener-Policy to same-origin. In that case, the opener communication will be severed.

Try doing the same example as previously, but then click on “go to google” and then back to the first page. You will see, when clicking on the button on the second page, that it will raise an error, because opener would have been nullified by the navigation to google.com.

You can reproduce the same behavior with a minimal example.

from flask import Flask, make_response

app = Flask(__name__)

@app.route("/")
def hello():
    response = make_response("Hello, <button onclick='window.history.back();'>good bye</button>")
    response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
    return response

if __name__ == "__main__":
    app.run(port=8001)

Try clicking on the second link that time.