Same origin restriction

The cornerstone of browser security is the "same-origin policy" (same-origin policy). Many developers know this, but they don't fully understand it.

Overview

Meaning

In 1995, the same-origin policy was introduced into browsers by Netscape. Currently, all browsers implement this policy.

Initially, it meant that the cookies set by the A webpage could not be opened on the B webpage unless the two webpages were "same source". The so-called "homologous" refers to "three identical."

-Same agreement -Same domain name -Same ports (this point can be ignored, see below for details)

For example, the URL of http://www.example.com/dir/page.html, the protocol is http://, the domain name is www.example.com, and the port is 80 (default The port can be omitted), its homology is as follows.

-http://www.example.com/dir2/other.html: Same origin -http://example.com/dir/other.html: different sources (different domain names) -http://v2.www.example.com/dir/other.html: different sources (different domain names) -http://www.example.com:81/dir/other.html: different sources (different ports) -https://www.example.com/dir/page.html: different sources (different protocols)

Note that the standard stipulates that URLs with different ports are not of the same origin (for example, port 8000 and port 8001 are not of the same origin), but the browser does not comply with this rule. In fact, different ports of the same domain can read cookies from each other.

Purpose

The purpose of the same-origin policy is to ensure the safety of user information and prevent malicious websites from stealing data.

Imagine a situation: A website is a bank. After the user logs in, the A website sets a cookie on the user's machine, which contains some private information. After the user leaves the A website, he visits the B website again. If there is no homology restriction, the B website can read the cookies of the A website, and the privacy is leaked. Even more frightening is that cookies are often used to save the user's login status. If the user does not log out, other websites can impersonate the user and do whatever they want. Because the browser also stipulates that submitting forms is not restricted by the same-origin policy.

This shows that the same-origin policy is necessary, otherwise cookies can be shared, and the Internet will be insecure.

Limit range

With the development of the Internet, the same-origin policy has become more and more stringent. Currently, there are three types of behaviors that are restricted if they are not of the same origin.

(1) Unable to read Cookies, LocalStorage and IndexedDB of non-same-origin webpages.

(2) Cannot access the DOM of non-same-origin web pages.

(3) AJAX request cannot be sent to a non-same source address (it can be sent, but the browser will refuse to accept the response).

In addition, the window objects of other windows can be obtained through JavaScript scripts. If it is a non-homogenous web page, currently allowing a window to touch the nine properties and four methods of the window object of other web pages.

-window.closed -window.frames -window.length -window.location -window.opener -window.parent -window.self -window.top -window.window -window.blur() -window.close() -window.focus() -window.postMessage()

Among the above nine attributes, only window.location is readable and writable, and all the other eight are read-only. Moreover, even if the location object is not of the same origin, it is only allowed to call the location.replace() method and write the location.href property.

Although these restrictions are necessary, they are sometimes inconvenient and reasonable use is affected. Here's how to circumvent the above restrictions.

A cookie is a small piece of information written by the server to the browser, and only web pages of the same origin can be shared. If the first-level domain names of two web pages are the same, but the second-level domain names are different, the browser allows sharing cookies by setting document.domain.

For example, the URL of page A is http://w1.example.com/a.html and the URL of page B is http://w2.example.com/b.html, so as long as the settings are the same document.domain, two web pages can share cookies. Because the browser uses the document.domain property to check whether it is of the same origin.

// Both web pages need to be set
document.domain = "example.com";

Note that both pages A and B need to set the document.domain property to achieve the same origin. Because setting document.domain will reset the port to null at the same time, so if you only set the document.domain of one webpage, the ports of the two URLs will be different, and the goal of the same source will not be achieved. .

Now, webpage A sets a cookie through a script.

document.cookie = "test1=hello";

This cookie can be read on the B webpage.

var allCookie = document.cookie;

Note that this method only applies to cookies and iframe windows. LocalStorage and IndexedDB cannot use this method to circumvent the same-origin policy. Instead, use the PostMessage API described below.

In addition, the server can also specify the domain name of the cookie as the first-level domain name when setting the cookie, such as .example.com.

Set-Cookie: key=value; domain=.example.com; path=/

In this case, the second-level domain name and third-level domain name can read this cookie without any settings.

iframe and multi-window communication

The iframe element can be embedded in other web pages in the current web page. Each iframe element forms its own window, that is, it has its own window object. The script in the iframe window can get the parent window and the child window. However, only in the case of the same source, the parent window and the child window can communicate; if they cross domains, they cannot get the other's DOM.

For example, if the parent window runs the following command, if the iframe window is not of the same origin, an error will be reported.

document.getElementById("myIFrame").contentWindow.document;
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

In the above command, the parent window wants to get the DOM of the child window, because cross-domain causes an error.

The opposite is also true, if the child window gets the DOM of the main window, an error will be reported.

window.parent.document.body;
// report an error

This situation applies not only to the iframe window, but also to the window opened by the window.open method. As long as it is cross-domain, the parent window and the child window cannot communicate.

If the first-level domain names of the two windows are the same, but the second-level domain names are different, then setting the document.domain property described in the previous section can circumvent the same-origin policy and get the DOM.

For websites with completely different sources, there are currently two methods to solve the communication problem of cross-domain windows.

-Fragment identifier -Cross-document messaging API

Fragment identifier

Fragment identifier refers to the part after the # of the URL, such as #fragment of http://example.com/x.html#fragment. If you only change the fragment identifier, the page will not refresh.

The parent window can write information into the fragment identifier of the child window.

var src = originURL + "#" + data;
document.getElementById("myIFrame").src = src;

In the above code, the parent window writes the information to be transmitted into the fragment identifier of the iframe window.

The child window is notified by listening to the hashchange event.

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

Similarly, the child window can also change the fragment identifier of the parent window.

parent.location.href = target + "#" + hash;

window.postMessage()

The above method belongs to cracking. In order to solve this problem, HTML5 introduces a brand new API: Cross-document messaging API (Cross-document messaging).

This API adds a new window.postMessage method to the window object, allowing cross-window communication, regardless of whether the two windows are of the same origin. For example, if the parent window aaa.com sends a message to the child window bbb.com, just call the postMessage method.

// The parent window opens a child window
var popup = window.open("http://bbb.com", "title");
// The parent window sends a message to the child window
popup.postMessage("Hello World!", "http://bbb.com");

The first parameter of the postMessage method is the specific message content, and the second parameter is the origin of the window receiving the message, that is, "protocol + domain name + port". It can also be set to *, which means that the domain name is not restricted and sent to all windows.

The way the child window sends a message to the parent window is similar.

// The child window sends a message to the parent window
window.opener.postMessage("Nice to see you", "http://aaa.com");

Both the parent window and the child window can monitor each other's messages through the message event.

// Both the parent window and the child window can use the following code,
// listen to message
window.addEventListener(
  "message",
  function (e) {
    console.log(e.data);
  },
  false
);

The parameter of the message event is the event object event, which provides the following three properties.

-event.source: the window to send the message -event.origin: the URL where the message is sent -event.data: message content

The following example is that the child window references the parent window through the event.source property, and then sends a message.

window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
  event.source.postMessage("Nice to see you!", "*");
}

There are a few things to pay attention to in the above code. First of all, there is no source of filtering information in the receiveMessage function, and messages sent from any URL will be processed. Second, the URL of the target window specified in the postMessage method is an asterisk, indicating that the message can be sent to any URL. Generally speaking, these two methods are not recommended because they are not safe enough and may be exploited maliciously.

The event.origin property can filter messages that are not sent to this window.

window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
  if (event.origin !== "http://aaa.com") return;
  if (event.data === "Hello World") {
    event.source.postMessage("Hello", event.origin);
  } else {
    console.log(event.data);
  }
}

LocalStorage

With window.postMessage, it is also possible to read and write the LocalStorage of other windows.

The following is an example, the main window writes the localStorage of the iframe child window.

window.onmessage = function (e) {
  if (e.origin !== "http://bbb.com") {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

In the above code, the child window writes the message sent from the parent window into its own LocalStorage.

The code for sending a message from the parent window is as follows.

var win = document.getElementsByTagName("iframe")[0].contentWindow;
var obj = { name: "Jack" };
win.postMessage(
  JSON.stringify({ key: "storage", data: obj }),
  "http://bbb.com"
);

The code for receiving messages in the enhanced sub-window is as follows.

window.onmessage = function (e) {
  if (e.origin !== "http://bbb.com") return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case "set":
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case "get":
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, "http://aaa.com");
      break;
    case "remove":
      localStorage.removeItem(payload.key);
      break;
  }
};

The enhanced version of the parent window sends the message code as follows.

var win = document.getElementsByTagName("iframe")[0].contentWindow;
var obj = { name: "Jack" };
// save to the object
win.postMessage(
  JSON.stringify({ key: "storage", method: "set", data: obj }),
  "http://bbb.com"
);
// read object
win.postMessage(JSON.stringify({ key: "storage", method: "get" }), "*");
window.onmessage = function (e) {
  if (e.origin != "http://aaa.com") return;
  console.log(JSON.parse(e.data).name);
};

AJAX

The same-origin policy stipulates that AJAX requests can only be sent to the same-origin URL, otherwise an error will be reported.

In addition to setting up a server proxy (the browser requests the same-origin server, and the latter requests external services), there are three ways to circumvent this restriction.

-JSONP -WebSocket -CORS

JSONP

JSONP is a common method for cross-origin communication between server and client. The biggest feature is simple and easy to use, no compatibility issues, all old browsers are supported, and the server-side transformation is very small.

Its approach is as follows.

The first step is to add a <script> element to the web page to request a script from the server. This is not restricted by the same-origin policy and can be requested across domains.

<script src="http://api.foo.com?callback=bar"></script>

Note that the requested script URL has a callback parameter (?callback=bar), which is used to tell the server the name of the client's callback function (bar).

In the second step, after the server receives the request, it splices a string, puts the JSON data in the function name, and returns it as a string (bar({...})).

In the third step, the client will parse the string returned by the server as a code, because the browser thinks that this is the script content requested by the <script> tag. At this time, as long as the client defines the bar() function, it can get the JSON data returned by the server in the body of the function.

Let's look at an example. First, the webpage dynamically inserts the <script> element, which makes a request to the cross-domain URL.

function addScriptTag(src) {
  var script = document.createElement("script");
  script.setAttribute("type", "text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag("http://example.com/ip?callback=foo");
};

function foo(data) {
  console.log("Your public IP address is: " + data.ip);
}

The above code sends a request to the server example.com by dynamically adding <script> elements. Note that the query string of this request has a callback parameter to specify the name of the callback function, which is necessary for JSONP.

After the server receives this request, it will return the data in the parameter position of the callback function.

foo({
  ip: "8.8.8.8",
});

The script requested by the <script> element is directly run as code. At this time, as long as the browser defines the foo function, the function will be called immediately. The JSON data as a parameter is treated as a JavaScript object, not a string, so the step of using JSON.parse is avoided.

WebSocket

WebSocket is a communication protocol that uses ws:// (non-encrypted) and wss:// (encrypted) as the protocol prefix. This protocol does not implement the same-origin policy, as long as the server supports it, cross-origin communication can be carried out through it.

The following is an example of the header information of the WebSocket request sent by the browser (taken from Wikipedia).

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

In the above code, one of the fields is Origin, which indicates the origin of the request, that is, which domain name it is sent from.

It is precisely because of the Origin field that WebSocket does not implement the same-origin policy. Because the server can determine whether to permit this communication based on this field. If the domain name is in the whitelist, the server will respond as follows.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

CORS

CORS is the abbreviation for Cross-Origin Resource Sharing. It is a W3C standard and a fundamental solution for cross-origin AJAX requests. Compared to JSONP, which can only send GET requests, CORS allows any type of request.

The next chapter will introduce in detail how to complete cross-origin AJAX requests through CORS.