Explain the Same Origin Policy thoroughly
/ 9 min read
The Pain of Front-End Newcomers
I would like to refer to the Same-origin policy (SOP, which may be abbreviated as SOP below) as the pain of front-end newcomers.
First, let’s briefly discuss what same-origin is: the same protocol, the same host, and the same port are considered the same origin. Taking http://example.com:80
as an example, the protocol is http
, the host is example.com
, and the port is 80
.
There are some strange restrictions when accessing resources from different origins, so let’s list these situations below together.
Restrictions
Canvas Pollution
Tainted (write-only) canvas, you cannot retrieve images from the canvas, similar situations also occur when loading webGL resources:
Accessing Information Inside Iframes
Accessing most of the information inside an iframe will be rejected:
Ajax Requests Fail
Finally, the most familiar to everyone, Ajax requests fail:
No Restrictions
Nevertheless, despite the many restrictions brought by the same-origin policy, there are still some ways to allow us to use resources from different origins:
- Links, redirects, and form submissions
<script src="…"></script>
<link rel="stylesheet" href="…">
<img>
,<video>
,<audio>
,<object>
,<embed>
tags- Fonts applied with @font-face
- Loading web pages in iframes (can be prevented through
X-Frame-Options
)
But why is that? Why can we access data using the above methods despite the strict same-origin policy? In fact, the essence of it all is—
Essence
In fact, all the unrestricted scenarios can only access data but not modify it. As the saying goes, you can look but you can’t touch.
All the methods mentioned above are only for read-only access. Anything you obtain cannot be modified. Opening a page in an iframe allows you to see the entire page, but you cannot interfere with the operation of the page’s program.
Among them, the form’s action is a historical legacy issue. It seems that using post to submit data is a modification operation, but in reality, it cannot access the returned data. Furthermore, after submission, it will directly redirect to the target URL, similar to links and redirects. Back in the day, this was heavily used during the PHP era.
So, for Ajax requests, the essence is also that non-same-origin resources cannot be written to (opaque response). Your request is successful, but the browser refuses to let you manipulate the retrieved data. In other words, intercepting cross-origin request results is a browser behavior.
The Purpose of Its Existence
Knowing the essence makes it easy to understand why SOP exists:
Translate into English:
- Network security issues in mainly prevent Cross-Site Request Forgery (CSRF)
- Prevent the host from arbitrarily operating iframes
- Prohibit clients from arbitrarily editing non-origin images, audio, and video files
Response Solutions
SOP brings secure user experience, but the development experience is not very good, and developers need to make extra efforts to deal with SOP.
CORS Protocol
To allow sharing responses cross-origin and allow for more versatile fetches than possible with HTML’s form element, the CORS protocol exists. It is layered on top of HTTP and allows responses to declare they can be shared with other origins.
The CORS protocol is used to negotiate whether server resources can be accessed by different origins.
That’s right, CORS is a protocol, a protocol based on the HTTP protocol, implemented through HTTP request and response headers. The specific process is as follows:
- Encountering a cross-origin request, and it is not a simple request (otherwise, the actual request is sent directly)
- Sending a preflight request
- The server returns CORS configuration information
- The browser determines whether to allow the actual request to be sent
- Sending the actual request, and the response headers of the actual request will also contain CORS configuration information, so the browser will allow developers to use the response data (if it is a simple request, it will be sent directly, and if the configuration information check fails at this time, a cross-origin error will be reported)
A preflight request (often translated as a preflight request) is an OPTIONS
HTTP request, with the key request headers being:
Access-Control-Request-Method
: Method of the actual requestAccess-Control-Request-Headers
: Headers included in the actual request
The CORS protocol will return the conditions for allowing cross-origin requests through HTTP response headers. From the naming, we can basically understand the functions of these response headers in the CORS protocol:
Access-Control-Allow-Methods
: Allowed methodsAccess-Control-Allow-Headers
: Allowed headersAccess-Control-Allow-Origin
: Allowed sourceAccess-Control-Allow-Credentials
: Whether to allow credentials to be carried when accessingAccess-Control-Max-Age
: Cache time for the above two pieces of informationAccess-Control-Expose-Headers
: Response headers that JavaScript can read
So why do we need preflight requests?
In my understanding, as mentioned earlier, the behavior of the browser is to intercept the result, and the request will still be successfully sent to the server and execute the logic normally, which is too dangerous. However, with preflight requests, the actual request is intercepted before it is successful in reaching the server. On the other hand, the CORS protocol does not block simple requests from reaching the server, probably because the GET method does not bring about data modification, making it difficult to cause serious consequences, combined with possible historical reasons, and hence is allowed.
Below is a simple implementation of a server-side CORS protocol:
A more mature solution can be found in fastify-cors.
Once the CORS protocol is negotiated, the browser will allow JavaScript to access cross-origin data. But the problem is not so easily solved (that’s why I said the development experience is not so good), even though cross-origin data can be read, there are still issues with credentials such as cookies:
The fetch
method itself does not, by default, carry cross-origin cookies, nor does it set the set-cookie
in the response. It must have credentials: "include"
set, but we need to do more than that, we still need to:
- After adding
Access-Control-Allow-Credentials
, allow requests sent to cross-origin to carry credentials - But after configuring
Allow-Credentials
,Access-Control-Allow-Origin
cannot be*
, it must be a singleOrigin
- Only
SameSite=None
cookies can be sent to the server - When
SameSite=None
,Secure
is also necessary - Must use the https protocol, otherwise
Secure
cookies cannot be written
P.S. There are subtle differences between same site and same origin, but in most cases they are not needed, for more details please refer to: The great SameSite confusion
Starting from chorme80 (2020.02), headers HTTP header: Set-Cookie: SameSite: Defaults to Lax
is introduced. At this point, users will feel that after the upgrade they can’t log in, it’s because cross-origin cookies are not sent by default.
By the way, there are also CORP request headers similar to CORS, this request header is used to forbid <script>
, <img>
and other resource references, with the default value being cross-origin
.
Today, browsers act as though Cross-Origin-Resource-Policy: cross-origin is set on every response that lacks an explicit CORP header.
Clean Canvas
The CORS protocol also applies to resolving the situation where images are read by the canvas. By default, the image request method is not CORS, and you need to add img.crossOrigin = 'anonymous'
(note that when added using JavaScript, camel case is required; all lowercase does not work, but when added to an HTML tag, it should be all lowercase) to change the request method.
The difference now is that although the canvas could originally display cross-origin images directly, after adding crossOrigin
, only resources with added CORS request headers can be successfully requested. Otherwise, a direct request will result in an error, and the image will not be displayed:
Successfully requested cross-origin images can be added to the canvas without polluting it.
Reverse Proxy
Apart from adding CORS, you can also use a reverse proxy to integrate originally cross-origin API services and network application files into the same domain.
In the development environment, you can use familiar tools such as Webpack devServer.proxy and Vite’s Server Options.
In production environments, nginx is a commonly used reverse proxy.
postMessage
postMessage
mainly addresses data exchange between different windows (e.g., different iframes):
Sender
To send information to another window using postMessage
, you first need to obtain the window
variable of the target window. For example: for an iframe, you can access the contentWindow
property after using querySelector
; the window.open()
method will directly return the window
object of the target window, and so on.
The first parameter is the data being sent, and it will be deeply cloned. Additionally, you can control the second parameter targetOrigin
to ensure that the origin of the target window is the value you specify.
In practical use, it might look like this:
Receiver
This approach works because it requires both parties to add encoding. Therefore, we can ensure that the communicating parties are trustworthy through some agreements. The simplest method is to use event.origin
to determine if the information source is from a trusted source, otherwise, it could be susceptible to attacks.
Websocket
Websocket can bypass the same-origin policy, but it puts a heavy load on the server. So, it is unlikely that anyone would use websocket to replace HTTP interfaces just to bypass the same-origin policy.