More than a few blog posts ago I stated my intent to publish a series of articles on cross-domain communication techniques. More time has passed than I had intended, but at last here is the start of that series of articles. The series will explore progressively more advanced cross-domain techniques as well as their strengths and weaknesses, culminating in an announcement about stuff we've been working on that I think you'll find interesting.
Cross-domain communication is usually discussed in the context of a browser client communicating to a web server that is different than the domain of the web page currently shown in the browser. The browser client displays a page from server foo.com, and that page tries to access data on server bar.com. This is forbidden by the same-origin browser security policy because bar.com isn't foo.com.
Server Side Proxy
One relatively simple way to resolve this is to have the browser page request data from the page's web server, and have the web server relay that request to the actual third party server. The browser displays a page from foo.com, and that page makes a data request to foo.com which foo.com relays to bar.com. Bar.com replies to foo.com, and foo.com forwards that response on to the browser client page to complete the circuit.
While this solution is simple and quite widespread today, it has some significant problems:
- Scalability and Network costs: The request and response travel across your server's network twice. Request in, request out, response in, response out. Traffic on your server network grows four times faster than growth of your application use. That means you'll reach network saturation four times sooner than with other techniques, and you'll pay four times more (in server network traffic costs) for the privilege.
- Impersonating the user: When your foo.com server makes a request to bar.com seeking data for the user, you're essentially impersonating the end user. If the data on bar.com requires any sort of user identification or authorization, your server side proxy suddenly jumps from super simple to super difficult. It's easy enough to ask the user to log in to bar.com, but your foo.com can't see anything that goes on in bar.com. In particular, foo.com cannot see whatever browser cookies that bar.com sets to indicate logged in state. Thus, it will be next to impossible for foo.com to present the appropriate cookies or credentials in its http request to bar.com to make bar.com believe that the request is coming from the legitimate user. And this should be difficult - this is nothing short of a man-in-the-middle attack on bar.com's security!
So, server side proxies are a quick and dirty way to toss anonymous data around, but they don't scale well and they hit a wall when the data requires authentication.
Web Sites With Subdomains
Web sites and web applications generally start out as simple beasts running on a single web domain (www.foo.com). As the site grows in functionality and complexity, the incentives to break that site up into subdomains (downloads.foo.com, feedback.food.com, images.foo.com) grows as well. Perhaps your web site has a download area that needs to be optimized for large file transfers. That would probably be easier to fine tune as a server or cluster dedicated to that function than to try to tune the entire web site for large file downloads.
Subdomains often sprout as a byproduct of a company's internal structure. It takes a lot more effort to coordinate updates to one central server shared by multiple departments on different schedules than for a department to own their own subdomain, nicely isolated from the rest of the company's constant revisions.
Double Edged Sword
Domain isolation is convenient to let you get your work done independently of the noise going on in the rest of the company's web presence, but also presents a new problem: web pages served from your subdomain cannot share information with web pages served from other subdomains of your company. If the user logs in to your company's main page, the browser cookies representing that login state are not accessible to your subdomain.
Lowering the Domain Barrier
The major browsers support a technique around this quandary, to allow subdomains to operate as equals within a common shared context. The HTML document object has a domain property which normally reflects the complete domain name of the server from which the HTML document was loaded. The browser will allow you to assign a subset of the current domain name to the document.domain property to indicate that you wish for the HTML document to be treated as though it were loaded from the parent domain.
The browser will (should) only allow you to change the document.domain to a less specific version of your current domain. one.two.three.foo.com could be lowered to foo.com, or could be lowered to three.foo.com.
The browser should not allow assignment of a top-level domain (domain suffix) to document.domain. You should not be able to change a document domain from "one.foo.com" to "com". There have been browser bugs in this area in the past where a browser implementer mistakenly interpreted "top level domain" to mean "the bit of the domain after the last dot". ".com", ".edu", and ".org" are top level domains, but ".co.uk" and ".co.jp" are TLDs also.
The browsers will not allow you to raise the domain of an HTML document to something more specific than its domain of origin, nor allow lateral domain shuttling. Changing document.domain from "two.foo.com" to one.two.foo.com" is forbidden. Changing a document.domain from "one.foo.com" to "two.foo.com" is forbidden.
Firefox 1.5 and 2.0 will not allow you to assign a domain name that is more specific than the document's current domain name under any circumstances. Once you lower one.foo.com to foo.com, it's stuck at foo.com forever. The only way to clear that state is to reload the page.
IE6 and IE7 will allow you to raise a document's domain back to it's actual domain of origin. If a page was served from one.foo.com, and you lower it to foo.com, IE will let you raise it back to one.foo.com. However, I've seen some instabilities and inconsistencies in the aftermath of "raising shields", so I don't recommend relying on this behavior. Since Firefox doesn't allow restoring domains to their original values, you should ignore the fact that IE sort of does allow it.
Bridging Silos Via Least Common Denominators
One or the Other, Not Both
XHR is restricted to connecting only to the current HTML page's domain of origin. Domain lowering applies only to the browser sandbox. XHR operates outside the browser sandbox, enforcing same-origin domain policy on its own. This leads to an interesting - and valuable - inconsistency:
XMLHttpRequest is not affected by domain lowering.
The Money Shot
This inconsistency in the handling of document.domain enforcement/awareness makes the following scenario possible: The logic of your web app runs in the context of a page loaded from one.foo.com, and you want to XHR load data from two.foo.com.
Here's how you do that:
- Make your html page A served from one.foo.com lower its document.domain to foo.com
- Implement a function GetData(callback) on this page that constructs an XHR request to load the desired data from two.foo.com. Wire up the XHR onReadyStateChanged to process the data completion using a function implemented in B.html, and in that function pass the received data to the callback function passed into GetData().
- Insert an invisible (1x1 pixel) iframe on page A and set its src to http://two.foo.com/B.html
Here Be Pixies. (Try Not To Piss Them Off)
The path through this murky realm is neither straight nor wide. If you take liberties or shortcuts with this recipe, be careful to test your code thoroughly on multiple browsers. Chances are high that any deviation will lead to failure on one of the browsers.
IE is fairly flexible in this area. You don't actually have to implement the GetData function in the B page. You can just construct in the A context an XHR object type from the B context and use it directly in the A context. ( var xhr = new bframe.window.XMLHttpRequest() ) For IE, the B page need only lower the domain to foo.com. After that, all the driving can be done from A.
This is also why step 3 above mandates that the XHR onReadyStateChange event handler should be wired to a function implemented in the B page - the native XHR object operating in the B context may have difficulty firing an event wired to a function in the A page context.
The Downside to Homogeneity
For domain lowering to work between two subdomains, both sides have to "lower their shields" to a common middle ground. As this technique catches on across departments and their corresponding subdomains, you can quickly reach a point where just about all the subdomains on the corporate web have provisions to lower their domain to the common corporate parent domain.
This risk grows with scale. The more subdomains you have that routinely lower their domains to the common ground, the greater the risk that one of them may be compromisable and serve as a beachhead to your entire network.
Tune In Next Time
There is a way to mitigate this weakest link risk such that an attacker compromising a weak subdomain does not get access to everything. This requires inverting some of the relationships presented in this article and making the silos deeper rather than shallower. I'll cover "Siloed Domain Lowering" in my next cross-domain article.