Chapter 9. Content Isolation Logic

Most of the security assurances provided by web browsers are meant to isolate documents based on their origin. The premise is simple: Two pages from different sources should not be allowed to interfere with each other. Actual practice can be more complicated, however, as no universal agreement exists about where a single document begins and ends or what constitutes a single origin. The result is a sometimes unpredictable patchwork of contradictory policies that don’t quite work well together but that can’t be tweaked without profoundly affecting all current legitimate uses of the Web.

These problems aside, there is also little clarity about what actions should be subject to security checks in the first place. It seems clear that some interactions, such as following a link, should be permitted without special restrictions as they are essential to the health of the entire ecosystem, and that others, such as modifying the contents of a page loaded in a separate window, should require a security check. But a large gray area exists between these extremes, and that middle ground often feels as if it’s governed more by a roll of the dice than by any unified plan. In these murky waters, vulnerabilities such as cross-site request forgery (see Chapter 4) abound.

It’s time to start exploring. Let’s roll a die of our own and kick off the journey with JavaScript.

The same-origin policy (SOP) is a concept introduced by Netscape in 1995 alongside JavaScript and the Document Object Model (DOM), just one year after the creation of HTTP cookies. The basic rule behind this policy is straightforward: Given any two separate JavaScript execution contexts, one should be able to access the DOM of the other only if the protocols, DNS names,[43] and port numbers associated with their host documents match exactly. All other cross-document JavaScript DOM access should fail.

The protocol-host-port tuple introduced by this algorithm is commonly referred to as origin. As a basis for a security policy, this is pretty robust: SOP is implemented across all modern browsers with a good degree of consistency and with only occasional bugs.[44] In fact, only Internet Explorer stands out, as it ignores the port number for the purpose of origin checks. This practice is somewhat less secure, particularly given the risk of having non-HTTP services running on a remote host for HTTP/0.9 web servers (see Chapter 3). But usually it makes no appreciable difference.

Table 9-1 illustrates the outcome of SOP checks in a variety of situations.

The simplicity of SOP is both a blessing and a curse. The mechanism is fairly easy to understand and not too hard to implement correctly, but its inflexibility can be a burden to web developers. In some contexts, the policy is too broad, making it impossible to, say, isolate home pages belonging to separate users (short of giving each a separate domain). In other cases, the opposite is true: The policy makes it difficult for legitimately cooperating sites (say, login.example.com and payments.example.com) to seamlessly exchange data.

Attempts to fix the first problem—to narrow down the concept of an origin—are usually bound to fail because of interactions with other explicit and hidden security controls in the browser. Attempts to broaden origins or facilitate cross-domain interactions are more common. The two broadly supported ways of achieving these goals are document.domain and postMessage(...), as discussed below.

This JavaScript property permits any two cooperating websites that share a common top-level domain (such as example.com, or even just .com) to agree that for the purpose of future same-origin checks, they want to be considered equivalent. For example, both login.example.com and payments.example.com may perform the following assignment:

document.domain = "example.com"

Setting this property overrides the usual hostname matching logic during same-origin policy checks. The protocols and port numbers still have to match, though; if they don’t, tweaking document.domain will not have the desired effect.

Both parties must explicitly opt in for this feature. Simply because login.example.com has set its document.domain to example.com does not mean that it will be allowed to access content originating from the website hosted at http://example.com/. That website needs to perform such an assignment, too, even if common sense would indicate that it is a no-op. This effect is symmetrical. Just as a page that sets document.domain will not be able to access pages that did not, the action of setting the property also renders the caller mostly (but not fully!)[45] out of reach of normal documents that previously would have been considered same-origin with it. Table 9-2 shows the effects of various values of document.domain.

Despite displaying a degree of complexity that hints at some special sort of cleverness, document.domain is not particularly safe. Its most significant weakness is that it invites unwelcome guests. After two parties mutually set this property to example.com, it is not simply the case that login.example.com and payments.example.com will be able to communicate; funny-cat-videos.example.com will be able to jump on the bandwagon as well. And because of the degree of access permitted between the pages, the integrity of any of the participating JavaScript contexts simply cannot be guaranteed to any realistic extent. In other words, touching document.domain inevitably entails tying the security of your page to the security of the weakest link in the entire domain. An extreme case of setting the value to *.com is essentially equivalent to assisted suicide.

The postMessage(...) API is an HTML5 extension that permits slightly less convenient but remarkably more secure communications between non-same-origin sites without automatically giving up the integrity of any of the parties involved. Today it is supported in all up-to-date browsers, although because it is fairly new, it is not found in Internet Explorer 6 or 7.

The mechanism permits a text message of any length to be sent to any window for which the sender holds a valid JavaScript handle (see Chapter 6). Although the same-origin policy has a number of gaps that permit similar functionality to be implemented by other means,[46] this one is actually safe to use. It allows the sender to specify what origins are permitted to receive the message in the first place (in case the URL of the target window has changed), and it provides the recipient with the identity of the sender so that the integrity of the channel can be ascertained easily. In contrast, legacy methods that rely on SOP loopholes usually don’t come with such assurances; if a particular action is permitted without robust security checks, it can usually also be triggered by a rogue third party and not just by the intended participants.

To illustrate the proper use of postMessage(...), consider a case in which a top-level document located at payments.example.com needs to obtain user login information for display purposes. To accomplish this, it loads a frame pointing to login.example.com. This frame can simply issue the following command:

parent.postMessage("user=bob", "https://payments.example.com");

The browser will deliver the message only if the embedding site indeed matches the specified, trusted origin. In order to securely process this response, the top-level document needs to use the following code:

// Register the intent to process incoming messages:
addEventListener("message", user_info, false);

// Handle actual data when it arrives:
function user_info(msg) {
  if (msg.origin == "https://login.example.com") {
    // Use msg.data as planned
  }
}

PostMessage(...) is a very robust mechanism that offers significant benefits over document.domain and over virtually all other guerrilla approaches that predate it; therefore, it should be used as often as possible. That said, it can still be misused. Consider the following check that looks for a substring in the domain name:

if (msg.origin.indexOf(".example.com") != −1) { ... }

As should be evident, this comparison will not only match sites within example.com but will also happily accept messages from www.example.com.bunnyoutlet.com. In all likelihood, you will stumble upon code like this more than once in your journeys. Such is life!

Note

Recent tweaks to HTML5 extended the postMessage(...) API to incorporate somewhat overengineered “ports” and “channels,” which are meant to facilitate stream-oriented communications between websites. Browser support for these features is currently very limited and their practical utility is unclear, but from the security standpoint, they do not appear to be of any special concern.

As we are wrapping up the overview of the DOM-based same-origin policy, it is important to note that it is in no way synchronized with ambient credentials, SSL state, network context, or many other potentially security-relevant parameters tracked by the browser. Any two windows or frames opened in a browser will remain same-origin with each other even if the user logs out from one account and logs into another, if the page switches from using a good HTTPS certificate to a bad one, and so on.

This lack of synchronization can contribute to the exploitability of other security bugs. For example, several sites do not protect their login forms against cross-site request forgery, permitting any third-party site to simply submit a username and a password and log the user into an attacker-controlled account. This may seem harmless at first, but when the content loaded in the browser before and after this operation is considered same-origin, the impact of normally ignored “self-inflicted” cross-site scripting vulnerabilities (i.e., ones where the owner of a particular account can target only himself) is suddenly much greater than it would previously appear. In the most basic scenario, the attacker may first open and keep a frame pointing to a sensitive page on the targeted site (e.g., http://www.fuzzybunnies.com/address_book.php) and then log the victim into the attacker-controlled account to execute self-XSS in an unrelated component of fuzzybunnies.com. Despite the change of HTTP credentials, the code injected in that latter step will have unconstrained access to the previously loaded frame, permitting data theft.



[43] This and most other browser security mechanisms are based on DNS labels, not on examining the underlying IP addresses. This has a curious consequence: If the IP of a particular host changes, the attacker may be able to talk to the new destination through the user’s browser, possibly engaging in abusive behaviors while hiding the true origin of the attack (unfortunate, not very interesting) or interacting with the victim's internal network, which normally would not be accessible due to the presence of a firewall (a much more problematic case). Intentional change of an IP for this purpose is known as DNS rebinding. Browsers try to mitigate DNS rebinding to some extent by, for example, caching DNS lookup results for a certain time (DNS pinning), but these defenses are imperfect.

[44] One significant source of same-origin policy bugs is having several separate URL-parsing routines in the browser code. If the parsing approach used in the HTTP stack differs from that used for determining JavaScript origins, problems may arise. Safari, in particular, combated a significant number of SOP bypass flaws caused by pathological URLs, including many of the inputs discussed in Chapter 2.

[45] For example, in Internet Explorer, it will still be possible for one page to navigate any other documents that were nominally same-origin but that became “isolated” after setting document.domain, to javascript: URLs. Doing so permits any JavaScript to execute in the context of such as a pseudo-isolated domain. On top of this, obviously nothing stops the originating page from simply setting its own document.domain to a value identical with that of the target in order to eliminate the boundary. In other words, the ability to make a document non-same-origin with other pages through document.domain should not be relied upon for anything even remotely serious or security relevant.

[46] More about this in Chapter 11, but the most notable example is that of encoding data in URL fragment identifiers. This is possible because navigating frames to a new URL is not subject to security restrictions in most cases, and navigation to a URL where only the fragment identifier changes does not actually trigger a page reload. Framed JavaScipt can simply poll location.hash and detect incoming messages this way.