Flite Careers

JavaScript's document.domain and How to Detect When It Changes

Whether you are a JavaScript developer, in Ad Ops, or in QA you’ve probably heard of this thing called document.domain. In JavaScript, the window’s document object has a property domain that when accessed typically returns the full host name of the site you’re running in. Go ahead, try it. Go to http://www.flite.com in your browser and in the developer tools, execute document.domain, and you’ll see that it returns www.flite.com. This property is used by the browser to determine if Site A and Site B can communicate with each other.

All browsers respect a security specification known as a Same-origin Policy, which means that sites and even iFrames can only communicate with eachother if they share the same origin or host. The document.domain property must match exactly in order for two frames to communicate with each other directly. I say “directly” because methods like using window.postMessage allow for Cross-document messaging which I’ll cover later in the post.

What can you do with document.domain?

Aside from reading the property, you can also set the property using JavaScript, but you may only set it to be less specific. For example, you can change www.flite.com to flite.com, but you cannot change flite.com to www.flite.com. There are some legitimate reasons why a site might make use of changing its document.domain property. One typical use case is for supporting a plugin across a site that has many subdomains. Here’s a fictional example: let’s use flite.com. We have several subdomains where different information is kept for different audiences. There’s the main marketing site www.flite.com, a site for developers at developer.flite.com, and a site to get help at support.flite.com. We also have a widget for serving videos that serves out of video.flite.com which happens to require access to the top frame in order to expand a sharing menu. In order for the video player to work on all three sites, we will either need to inline the video player, enable Cross-Origin Resource Sharing (CORS) on our servers, or we could set the document.domain property in all four of our sites using document.domain = "flite.com". Magically, everything works and we can move along with our lives. This method is all well and good if you are the one controlling each of the sites and can orchestrate this sort of change, but it can wreak havoc on ads and plugins running on your site if they aren’t prepared to handle it.

So, what’s the problem?

Flite Ads are 3rd party and we don’t have control over how sites are implemented. In some implementations, our ads are served in iFrames that share the same domain as the top page where we need to expand into. Under normal circumstances the document.domain property is not changing. In this ideal scenario, we continue to have the ability to expand onto the top page. Recently, we have been seeing a trend among sites where their JavaScript, or some plugin the site is loading, changes the document.domain property of the page. Sometimes it changes before the ad is loaded, sometimes shortly after the ad is loaded, or sometimes not at all. This makes it impossible to always change the document.domain property by default. Instead, our code running inside of the iFrame must try and detect when the parent’s document.domain property changes and update its own in lock-step so that during any future attempts to expand, our code will get the access it may need.

Flite’s secret sauce…

I mentioned 2 scenarios, the first being where document.domain is changed before our ad is loaded. To overcome this, we simply try to determine if we can access the top window’s document object and if not, we change our local document.domain property and try again.

Shifting document.domain using trial and error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Function to check if current window and the top window are Cross-Origin
var isCrossOrigin = function() {
    try{
        //try to access the document object
        if (top.document || top.document.domain){
          //we have the same document.domain value!
        }
    }catch(e) {
      //We don't have access, it's cross-origin!
        return true;
    }
    return false;
};

//Function to shift off the first part of the host name
var shiftDomain = function() {
  var currentDomain = document.domain;
  document.domain = currentDomain.substring(currentDomain.indexOf('.') + 1);
};

//Reduces 2 subdomains down to just the domain only if necessary 
//(e.g. dev.support.flite.com to flite.com)
if (isCrossOrigin()) {
     shiftDomain();   
     
     if (isCrossOrigin()) {
      shiftDomain();
     }
}

The problem with this method is that while it will work and allow you to communicate between frames, all browsers will complain about the first attempt to access the document by throwing an error in the JavaScript console. Even though the code is catching the error, the browser will still report it. It won’t halt execution of subesequent code, but it will cause an error message stating: Unsafe JavaScript attempt to access frame with URL 'blah' from frame with URL 'blah'. Domains, protocols and ports must match.

The second case is what we’ve been encountering more of: the site loads a widget that is loaded after our ad is loaded which requires changing the document.domain property. This bypasses our initial scenario leaving us with a mis-matched document.domain property after we’ve initialized. As soon as this property changes, direct communication between frames in either direction is cut off. As I mentioned earlier, we are able to overcome this. On initialization we create a polling loop using createInterval in the top window that waits until the document.domain property changes from what it once was. Once the change is detected, it clears the interval and executes a call to window.postMessage which sends a message to our frame. We send along the new value in the message and our frame is ready waiting for this message. Upon receiving it, the frame changes the local document.domain property which restores the ability to communicate to the parent frame and vise versa!

Tracking when document.domain changes gracefully:

  • JSFiddle showing how when document.domain changes, a child page can no longer access it. The top box is the current frame, the blue box is the child iFrame, both are loaded from the same subdomain:
  • The child frame used in the previous example. It displays the current frame’s and the parent frame’s document.domain values:
  • JSFiddle showing how a child frame can detect the change and handle it gracefully:
  • The child frame with code that tracks document.domain changes in the parent frame:

Final thoughts

If I could have my way, I would push everyone towards abandoning using this property. The reality is that all modern browsers support CORS and we as an industry should be embracing it. It’s one of those things that your developers want to use, but your Operations team likely needs to enable. The Web as a whole moves at glacial speeds when adopting new standards, so while it would be ideal to have everyone move to CORS, it’s not practical. I’ve shown that there are ways to work around it that make your plugin a little more resilient out in The Wild. However, if you develop a plugin or use a plugin on your site that requires changing document.domain, please consider enabling CORS and please stop changing document.domain! As a final note, on Flite.com and all of its subdomains, we do not change the document.domain property, I just needed an example :-).


Comments