Embedding iframes – Static, Third-Party, and Dynamic Options

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    May. 31, 2019
    Last Updated:
    Jul. 17, 2019
  • classTags
    classClick for Tags

I feel like iframes are a very under-appreciated part of the web-development world. Especially as concern grows about the amount of invasive tracking and its negative effect on page performance, iframes continue to be an excellent way to force third-party code into sandboxes, where they can’t interact with the host page (unless explicit data passing has been setup).

I personally use iframes somewhat often, but infrequent enough that I often forgot things like “can I force this site’s widget into an iframe?”, or “what is the fastest way to throw this into a publicly accessible iframe embed?”. This post is a way for me to organize some notes on the topic and keep a list of resources related to iframe embeds, specifically for developers.

Table of Contents:

The Basics: Static HTML upload

I would be remiss if I didn’t mention the default way to host up an iframe that others can embed; simply throw your code into a static HTML file (or generate with server-side code, more on that later), and shove the file onto your publicly accessible host (shared host, VPS, AWS bucket, etc). Lets pretend you uploaded it to the root of your domain, at example.com/widget-A.html. Then to embed, you could simply pull it in via the standard iframe element: <iframe src="/widget-A.html"></iframe>.

I use this simple method a few times throughout this site, so that I can pull some custom widgets into a WordPress page and not have to worry about conflicting CSS or messing with HTML escaping in WordPress post content.

This is always a good fall-back method to wrap anything in an iframe. However, if code relies on access to JS globals or things like window.location.href, you might need to add some extra code to explicitly pass data from the parent window to the embed HTML file. And of course this doesn’t help at all really with dynamic iframe generation, where you want to put user submitted content within iframes.

Third-Party Generators:

If you are a web developer that likes to share widgets, you are probably already familiar with sites like jsFiddle and CodePen. However, you might not know that many of these sites allow for direct embedding, and some even allow you to natively use iframes as the embed type. However, please note that the intended purpose here is to embed code snippets to share with other developers or show off little demos. Embedding entire applications or exceeding what is considered “normal use” is likely against the TOS and is not a good idea anyway for anything beyond proof-of-concept demos and the such.

jsFiddle – An Interesting Lesson in Iframe Mitigation

Inside the JSFiddle editor, there is an “Embed” button, which lets you configure an embedded widget that you can copy the code for and paste into your destination of choice. You can easily configure it to generate an embeddable iframe:

JSFiddle IFrame Embed Option

The resulting Iframe can be set to just show the “result” of the fiddle, which is desirable if you are trying to show off a demo to users, rather than to other developers. However, JSFiddle injects a top menu bar into all Iframes with a tab selector and “Edit in JSFiddle” text, regardless of how many tabs are set to display:

JSFiddle IFrame Embed - Result Only - Top Bar Included

You might have noticed that the iframe embed code generated by JSFiddle uses a src of “jsfiddle.net/{{user}}/{{fiddleID}}/embedded/result/”, but the window.location.href in our fiddle shows that what actually loaded was “fiddle.jshell.net/{{user}}/{{fiddleID}}/show/{{theme}}”. Those trying to get rid of the injected menu bar might try setting the iframe source manually to the “fiddle.jshell.net” URL, but would be disappointed to see that the menu bar still gets injected. What gives? How is JSFiddle doing this?

The answer is interesting, and outlines a good technique for blocking users from abusing an iframe embed system. First, JSFiddle uses a “wrapper” around the final result, which is to say that it double-nests iframes. So the setup looks kind of like this:

  • Level A: Your Website
    • Level B: JSFiddle default IFrame Embed (<iframe src=”jsfiddle.net/…/…/embedded/result”></iframe>)
      • [Automatic] Wrapper(html)
        • Tabs Bar (html)
        • Level C: Secondary iframe (<iframe src=”fiddle.jshell.net/…/…/show/…”></iframe>)
          • Actual JSFiddle Result

Second, and this is the interesting part, it relies on the fact that when resources are loaded, either through AJAX or through resource tags like <script src=””>, the browser automatically includes what website the request is coming from, as the “referer” header. If a request comes from within an Iframe, the referrer matches the host of the Iframe content, not the site that the Iframe is embedded on. JSFiddle is checking this, and if the request is coming from a website outside of its control, it knows that the wrapper/tabs/secondary iframe must not be injected yet, since the wrapper should be loaded inside an iframe that would send a JSFiddle controlled domain as the referrer. Since the tab bar is injected into a wrapper that contains the fiddle result in an iframe, rather than directly into the fiddle result, it also means that your fiddle code can’t modify the wrapper, so you can’t do something like set “display:none” on the tab bar through your fiddle code.

To summarize:

  • HTTP Request to fiddle.jshell.net/…/…/show/
    • …From a JSFiddle domain:
      • Response HTML will be your JSFiddle code
    • …From outside a JSFiddle domain:
      • Response HTML will contain injector code that creates wrapper, tabs, and secondary iframe that requests fiddle.jshell.net/…/…/show/ all over again.

I’m not going to go into detail on how to remove the top bar, since that isn’t really something you should be doing, but I will point out that if you set your original Iframe embed src to fiddle.jshell.net/…/…/show instead of the default embed src, then the secondary iframe will actually have the same origin as the wrapper, which means that you can access and modify it with “window.parent.document” from inside your actual Fiddle code.

CodePen: Don’t Mess with Us!

Like JSFiddle, CodePen also has an IFrame embed option. In fact, they pretty much use exactly the same methods for blocking abuse of the IFrame and removal of the menu and “Edit on CodePen” text:

  • Nested iframes: “codepen.io/…/embed/…” loads a wrapper, with the menu bar, which in turn nests a secondary iframe with the result at “s.codepen.io/…/fullembedgrid/…”
    • Because the secondary Iframe has a different domain (subdomain), your CodePen code cannot touch the menu bar in the first, due to cross-origin policy.
  • CodePen checks the referer header on the result Iframe src

Here is where it is different. Whereas JSFiddle will re-embed the iframe if it detects a mis-matched referrer, CodePen will simply block your request to load the result entirely if it detects the referer not matching a CodePen owned host. If you try to set “s.codepen.io/…/fullembedgrid/…” as the source of an iframe that is not inside your fiddle, you will see this:

CodePen IFrame Result Embed - Blocking External Referrers

To be clear, this error message is misleading; the browser is sending a referer header, it just isn’t one that CodePen wants to see. What this error message should really say is something like “We see what you are trying to do… cut it out!”.

JS Bin: Ajax Fine, Browser Not

JS Bin is a little different than CodePen and JSFiddle in how it treats embeds. It looks like they do allow embeds, but only for Pro users. Also, instead of checking the referer header to block cross-origin result embedding, they are using X-Frame-Options, which is a cleaner and less error-prone way of blocking cross-origin embeds, but only blocks in browsers that support it. JS Bin has set X-Frame-Options on their “output” subdomain to “sameorigin”, which as its name would imply, will block the embed if the origin if the frame does not match the origin of the page.

I noticed kind of a strange thing with JS Bin, which is that they are not enforcing a referer match and they actually are allowing cross-origin requests. This probably has to do with their live-streaming / hot-reloading features. Anyways, this strange combination of security means that this actually worked for me:

fetch('https://output.jsbin.com/{{ID}}').then(function(response){
    response.text().then(function(html){
        var iframe = document.createElement('iframe');
        document.body.appendChild(iframe);
        iframe.contentWindow.document.open();
        iframe.contentWindow.document.write(html);
        iframe.contentWindow.document.close();
    });
});

Advanced: Dynamic iframe content injection with client-side Javascript or inline attributes

One thing that you might not be aware of is that you can actually use iframes that show content that is not from an actual domain – e.g. dynamically generated content with Javascript or by inline attributes. There are tons of caveats to using iframes in this way though, and I’ll cover them for each of the available methods to dynamically generate iframe content client-side. These are listed in order from most compatible but least friendly, to most friendly but least compatible.

“iframe.contentWindow.document.write” – Maximum cross-compatibility

This is pretty much the only solution that works for IE, since things like srcdoc and data-uri do not (or have very limited support), and I found it through this StackOverflow answer. This method works by creating a new iframe element that has the same origin as the current page and then streams raw HTML text into the document of that iframe with document.write(html):

function sameOriginIframeInject(html,target,append,cb){
    cb = typeof(cb)==='function' ? cb : function(){};
    var iframe = document.createElement('iframe');
    if (append){
        target.appendChild(iframe);
        iframe.style.width = '100%';
        iframe.style.height = '100%';
    }
    else {
        target.replaceWith(iframe);
    }
    iframe.addEventListener('load',cb);
    iframe.contentWindow.document.open();
    iframe.contentWindow.document.write(html);
    iframe.contentWindow.document.close();
}

// Example:
sameOriginIframeInject('<script>alert("success!");<\/script>',document.querySelector('#iframeWrapper'),true);

The downside to this method is that it pretty much negates the entire point of iframes in the first place; because the iframe shares origin with the parent, any code you put in the iframe can access your site, and vice-versa. However, standard iframe benefits of separately scoped JS variables and scoped CSS still apply, which is probably why this method still gets some use.

BONUS Script: Inject a cross-origin URL into a same-origin iframe. This will let you do some crazy stuff, like use iframe.contentWindow.print(), which would normally be unusable with an iframe that displays cross-origin content. Note that this method is very hackish and is likely to break on a large portion of sites.

function injectSameOriginIframeFromRemote(remoteSrcUrl, target, append, OPT_cb, OPT_useProxy) {
    var PROXY_BASE = 'https://cors-anywhere.herokuapp.com';
    useProxy = typeof(OPT_useProxy)==='boolean' ? OPT_useProxy : true;
    cb = typeof (OPT_cb) === 'function' ? OPT_cb : function () { };
    var iframe = document.createElement('iframe');
    if (append) {
        target.appendChild(iframe);
        iframe.style.width = '100%';
        iframe.style.height = '100%';
    }
    else {
        target.replaceWith(iframe);
    }
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4){
            if (xhr.status === 200){
                var rawHtml = xhr.responseText;
                var baseString = '';
                rawHtml = rawHtml.replace(/]+>/gim,'');
                rawHtml = rawHtml.replace(/<\/head>/gim,baseString + '\n' + '');
                iframe.contentWindow.document.open();
                iframe.contentWindow.document.write(rawHtml);
                iframe.contentWindow.document.close();
            }
            else {
                cb(false);
            }
        }
    }
    xhr.open('GET',PROXY_BASE + '/' + remoteSrcUrl);
    xhr.send();
    iframe.addEventListener('load', cb);
}

Data URI as Src attribute: Hackish, but works

As a method that is in-between the old way (same-origin contentWindow.document.write) and the newest method (srcdoc), using a Data URI as the src of an iframe element is a solution that has decent browser support, but also decent limitations.

What is a Data URI? – A Data URI is, well… data as a URI. Ok, I know, that’s not very helpful, but that really is all it is. Normally URIs are strings that identify a resource, and they often they “point” to another location – URLs are a form of URIs that point to web addresses. Or “tel:{{number}}” is a URI that identifies a phone number. Data URIs are unique in that instead of “pointing” to the location of the data, they actually have the raw data inside of the URI. An analogy would be that a book could be theoretically shared by an ISBN URI of “ISBN 0-123-45678-9”, but a Data URI that represents a book could be something like “text/book;charset=US-ASCII,{{ENTIRE_TEXT_OF_BOOK}}”.

The way that we can use Data URIs with iframes is by setting the src of the iframe element to a data URI that holds raw HTML content, rather than to a URL that points to a web address. Sample:

function dataUriIframeInject(html,target,append,cb){
    cb = typeof(cb)==='function' ? cb : function(){};
    append = typeof(append)==='boolean' ? append : true;
    var iframe = document.createElement('iframe');
    // Construct Data URI
    var dataUri = "data:text/html;charset=utf-8," + escape(html);
    iframe.addEventListener('load',cb);
    iframe.setAttribute('src',dataUri);
    if (append){
        target.appendChild(iframe);
        iframe.style.width = '100%';
        iframe.style.height = '100%';
    }
    else {
        target.replaceWith(iframe);
    }
}

There are several downsides to using Data URIs, and especially with IFrames

  • Maximum length
    • This is not standardized across browser, and sometimes is specified as a maximum character count, and sometimes as a maximum byte size (which would mean max chars depends on encoding type).
    • Good rule of thumb is probably to stay under 2K characters.
    • See this StackOverflow answer, and this page.
  • Limited browser support
    • Data URIs are supported on IE starting with IE8, but only for assets like images and CSS, not HTML. That makes it unusable with iframes on IE, even up to version 11.
    • See caniuse.com/#feat=datauri for more details.
  • CachingĀ 
    • The page that contains the Data URI can be cached, but the actual Data URI is not cached itself.
  • Character escaping
    • Trying to escape strings is notoriously annoying, and although functions like escape() make this easy for the most part, don’t be surprised if you run into something not working like you were expecting it to.
  • Limited functionality, and limits are different across browsers
    • It can be hard to find this kind of info, but there are special security restrictions with content loaded through a data URI. For example, in Chrome, reading and writing cookies is blocked/disabled inside data URIs. Even first-party same-origin cookies.

The main benefit of injecting content into iframes with data URIs over the previous method is that, even though the iframe is essentially loading a virtual location, it maintains a separate origin from the document it is embedded into! This means that cross-origin policies are enforced, and javascript inside of the iframe cannot access your website, and vice-versa. However, be aware that some old versions of browsers might actually treat them as same-origin (or more accurately, inheriting the origin of the parent). For example, it looks like Firefox versions below 57 did so.

Srcdoc attribute specifically for iframes: the hip new thing

Srcdoc is an attribute that you can use on iframes as an alternative to setting src. You set Srcdoc to a string of (valid) HTML, and it will load it into the iframe:

<iframe srcdoc="<p>Hello World!</p>"></iframe>

Like the Data URI method, the iframe maintains a separate origin than the page it is embedded into, so cross-origin policies are enforced and javascript cannot break out of the iframe and interact with the parent, and vice-versa.

Srcdoc is basically the official solution that all of the previous methods tried to hack together unofficially. It is super easy to use, has its own spec, and is gaining in adoption. Furthermore, it seems to have better security controls and documentation on how it sandboxes content. Unfortunately, the main drawback is still browser-compatibility. In fact, srcdoc has zero support on all versions of IE, and only proposed support on Edge. See caniuse.com/#feat=iframe-srcdoc for a full breakdown of browser support. You also still have to be somewhat careful about escaping strings – for example, the specs say that ampersands need to be double escaped.

If you want to use srcdoc, but support as many browsers as possible, you might want to look at this polyfill. It lets you use srcdoc normally without worrying about which browsers your users are using, and then goes through and switches to an older method if necessary for the user.

Generating iframe content server-side:

Because of the complexities and more error-prone nature of generating iframes on the client-side with Javascript, or by serving iframes with inline Data URI srcs or srcdocs, a lot of people choose to generate iframe content on the server and use the src attribute of the iframe to point to the generator endpoint with information on what to serve. For example, consider a large commenting system where users can leave comments with embedded code demos – definitely something you want to enforce security on so users can’t break out of the comment area and interact with the DOM or client JS. For this example, you might do something like:

  • All comments are stored in a DB with unique IDs
  • Create a generator script that has an endpoint at:
    • "embeds.example.com/comments/"
  • In the comment area, embed iframes with corresponding comment ids, like:
    • <iframe src="embeds.example.com/comments/?id=220"></iframe>
  • Your script would pick up the ID from the querystring, lookup the comment from the DB, and echo back the raw HTML as a document, which would load into the iframe.

Of course, there are many different ways to do this, and no one way is the “correct way”, but when it comes to security, you definitely want to be careful. If you don’t need users to be able to embed any code, and just want simple comments, you would not use iframes, and instead just escape out any HTML and JS users try to put in comments. Again, this is complex stuff, so you probably just want to find a reputable commenting system rather than rolling your own. Improper escaping of comment forms and XSS (cross-site-scripting) vulnerabilities have plagued even the largest companies.

Leave a Reply

Your email address will not be published. Required fields are marked *