I will talk about some edge cases of the Same-Origin Policy (SOP). It affects browser based thickclient platforms so it's not just for web application security. This is a more detailed dive into this topic that I touched briefly in the localghost talk.
If you know the foundations please jump directly to the SOP Gone Wild section below.
These definitions are not exactly correct and I have omitted some exceptions. SOP is one of the most well-studied topics in the browser security model so there is ample reading material.
The Origin Header
origin header is set by the browser for cross-origin requests. It contains
the origin of the request. An origin has three parts:
- Protocol or scheme: Usually
- Domain: E.g.,
whatever.example.net. Domain does not include the path.
- Port: Usually omitted. Assume the default port not present. E.g., 443 for
https. Internet Explorer does not care about port.
Origin is a forbidden header. Forbidden headers are set by browsers and cannot
Same-Origin Policy Simplified
SOP means a script from one origin cannot send most requests to another origin or read the responses to cross-origin requests that were sent absent any other mechanism (e.g., CORS). For more information please read:
Cross-Origin Resource Sharing (CORS)
CORS allows a script from an origin to bypass the SOP and interact with another
origin. This usually happens when the server on the other side adds a bunch of
headers to the response. The most important header is
Access-Control-Allow-Origin1. If this header contains the value of the
sender's origin then the browser allows the sender to see the response or in
some cases actually send a request to the other side.
If this header is missing then CORS is not enabled. The value of this header can be:
- An exact origin (or number of origins) like
*that matches everything.
In practice, the remote server looks at the
Origin header in the incoming
request. If it's in the allowlist then the response will contain the exact
origin in the value of the
Access-Control-Allow-Origin header in the response.
Burp's scanner has a simple check for this. It sets the
Origin header to some
arbitrary value. If the response contains that value or the
* wild card then
it creates and issue.
The browser allows an origin to send "simple" requests to another origin without any checks2. The request goes through but in the absence of CORS headers the browser might not let the sender see the response.
A simple request is:
- Only has one of these three methods:
- Only has certain headers that are not set by the browser. The only important
header for this discussion is
Content-Type. It can only contain these values:
There are more requirements but they are not important here. More information:
Other requests are not sent without checks. The browser sends an
request with some headers to the endpoint and reads the response headers. If
these headers allow CORS then the browser sends the actual request.
This can become an unintentional CSRF protection. If your webapp uses POST
requests with JSON payloads. The requests will have
This means without CORS these POST requests are not vulnerable to CSRF. Because
when phished users click on links in a typical CSRF scenario, the browser sends
the preflight request which fails and the actual CSRF request is never sent.
For more information please see:
A couple of quick tricks:
- Change the
text/plain(or remove the header completely) to make it a simple request.
- Change the verb of the request to
- Change the payload from JSON to
param1=val1¶m2=val2. This works only occasionally and nested JSON objects do not work. But it's worth a try.
SOP Gone Wild
Let's talk about the edge cases.
Origin Does Not Always Have Three Items
Origin consists of protocol, domain, and port. Internet Explorer does not care about port.
In addition, Internet Explorer does not care about SOP when dealing with
Trusted Zones. In most corporate environments the internal domains are added to
If The Port Is Missing From the Origin Then the Default Port Is Implied
https://example.net are effectively the same. I
thought I had found a gotcha moment but I was wrong.
WebSockets Are Not Bound By The SOP
This is a common issue and the most important item in this blog. Websockets
start with a handshake. The handshake is a GET request which satisfies the
simple request criteria. So it's sent cross-origin.
If the request is cross-origin and no CORS policy is defined, the sender cannot see the response of the handshake. But it really does not matter. The browser does it for us.
For more information please read a section from this URL by Independent Security Evaluators:
A couple of tricks:
Sec-WebSocket-Keyin the handshake request has nothing to do with security.
- One of the headers in the request is
Sec-Websocket-Protocol. Sometimes you can change the protocol of the websocket with this header. An application was using protobuf. I noticed the value of this header in the request is also set to
protobufso I changed this value to
jsonand it switched to neatly formatted JSON.
The Origin Header Is Not Always Set
The browsers only set the
origin header for some requests. Generally, the
header is only set for cross-origin requests. This is not completely correct but
going into the details will just complicate things.
Cross-Origin Simple Requests Are Sent Without Checks
We all already know this. However, because the browser does not allow access to the response we think the request is not valid. GET requests and some POST requests are sent anyways. We might not be able to see the response but the action is probably already executed. Hence, why CSRF exists even if there is no CORS because the POST request performs some action.
Look at this bug from TavisO at https://bugs.chromium.org/p/project-zero/issues/detail?id=693.
TrendMicro was running a local webserver. You can execute commands by sending a
GET request like
You will not be able to see the response but the code is already executed and
you got remote code execution.
A similar issue happened in my Attack Surface Analyzer RCE. A "simple" GET request was used to inject the XSS to RCE payload in an Electron app.