How MIME Type Handling in Go Can Lead to Unintended File Downloads

Introduction

While researching a simple Go application, I discovered a behavior that could lead to unintended file downloads, potentially allowing attackers to deliver custom content with execution capabilities. The issue arises due to Go’s automatic MIME type detection, which misinterprets certain URL inputs and causes browsers to download files as application/octet-stream.

This article goes over the discovery process, the possible implications of this behavior, and how to mitigate it.

All the tests were performed with Go 1.23.6.

The Issue: How Go Handles Content-Type Automatically

When writing data to the http.ResponseWriter without explicitly setting the Content-Type, Go’s net/http uses the http.DetectContentType function to determine the MIME type based on the initial bytes (the first 512 bytes) of the data. If it cannot determine a specific MIME type, it defaults to: Content-Type: application/octet-stream [Go net/http Default Content-Type Handling]. This instructs the browser to treat the response as a binary file, triggering an automatic download.

To validate this, I created a simple URL redirection server that maps short paths (e.g., /google) to full URLs (https://www.google.com). When a user accesses a known path, the server automatically redirects them to the corresponding website.

package main
import (
"fmt"
"log"
"net/http"
"path"
)
// Map of short paths to full URLs
var redirects = map[string]string{
"/google": "https://www.google.com",
"/github": "https://github.com",
"/reddit": "https://www.reddit.com",
"/docs": "https://go.dev/doc/",
"/youtube": "https://www.youtube.com",
}
func handler(w http.ResponseWriter, r *http.Request) {
path := path.Clean(r.URL.Path)
log.Printf("Received request for: %q", path)
// Check if the path exists in the redirection map
if target, exists := redirects[path]; exists {
log.Printf("Redirecting %q to %q", path, target)
http.Redirect(w, r, target, http.StatusFound) // 302 Found
return
}
// Default response for unknown paths
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Not authorized path: %s", path)
}
func main() {
http.HandleFunc("/", handler)
port := ":8080"
log.Printf("Starting server on %s\n", port)
log.Fatal(http.ListenAndServe(port, http.DefaultServeMux))
}

Without knowing the code, it would pop to mind testing for some potential security issues, specifically Cross-Site Scripting (XSS), due to the reflection of the input that is passed on the URL path.

For the sake of testing, let’s see how the server responds to two simple examples.

In the first example, we simply request the word path, getting the response reflecting our word in text/plain.

Figure 1: Simple random path request

In the second example, we requested an XSS payload p><img src=1 onerror=alert(1)> that would attempt to influence the response to be of Content-Type text/html. However, since the first bytes are strings, the Content-Type detection will be text/plain. Avoiding the potential XSS.

Figure 2: Request to test for XSS

Unexpected Behavior Leading to Unintended Downloads

During testing, I observed that when certain special characters were passed in the URL, Go assigned the application/octet-stream content type, causing some browsers to download the response as a file instead of displaying it.

This was discovered through fuzz testing and analyzing server responses.

Figure 3: Fuzzing sneak peek
Figure 4: File download from fuzz testing payloads

Example: File Download from a Specially Crafted URL

To illustrate how this simple Content-Type handling could be abused, I created a simple payload that, when accessed via the URL, creates a bat file. When executed, this file prints a message and pops our dear calculator.

%0A%0Decho Your system will be updated;%0A%0Dpause;%0A%0DC:/Windows/System32/calc.exe%20%00%0Fupdate.bat
https://vulnerable-application.com/%0A%0Decho Your system will be updated;%0A%0Dpause;%0A%0DC:/Windows/System32/calc.exe%20%00%0Fupdate.bat

The above URL, when accessed, will create a file named calc.exe _00_0Fupdate.bat with the following content:


echo Your system will be updated;
pause;
C:/Windows/System32/calc.exe update.bat
Figure 5: Automatic download (Firefox)
Figure 6: Execution of the unintended file download

This is just a simple example to represent the potential of such an issue. However, exploiting this issue primarily relies on social engineering, which increases the attack complexity.

In some cases, an attacker could exploit file extension confusion, increasing the likelihood of execution. In the below example, a file named calc.exe _00bills.pdf is created:

%0A%0Decho Your system will be updated;%0A%0Dpause;%0A%0DC:/Windows/System32/calc.exe%20%00bills.pdf.exe
Figure 7: File Extension Confusion

Attackers can exploit file extension confusion to disguise executable files as harmless documents. Because Windows hides known file extensions by default, users may mistakenly open what appears to be a .pdf but is actually an .exe—executing malicious code.

Execution Outcome:

  1. The browser downloads calc.exe_00_00bills.pdf.
  2. If the user opens it, the script executes:
  • Prints Your system will be updated
  • Launches Windows Calculator (calc.exe)

Additionally, this issue could be leveraged in additional attack scenarios:

  • Phishing Attack: An attacker disguises a link as a trusted source, causing a malicious file download.
  • Drive-By Download Attack: A compromised website uses this vulnerability to trick users into downloading and executing a harmful script.

Browser-Specific Behaviors

The main tests described here were performed on the Firefox browser. However, for an understanding of the behavior in other browsers, Chrome and Edge were also tested.

Browser Behavior when handling application/octet-stream responses

The observed differences in browser behaviors when handling responses with the application/octet-stream MIME type stem from each browser’s security policies and handling of unrecognized content types. The table above summarizes my testing results. Additional research would be necessary to dig into additional details and understanding.

Mitigation Strategies

Explicitly Set Content-Type

One simple way to prevent this issue is to modify the Go server to explicitly return text/plain:

w.Header().Set("Content-Type", "text/plain; charset=utf-8")

This ensures all responses are treated as plaintext, blocking unintended file downloads.

Figure 8: Fuzzing Validation after explicit set content-type

Escaping Special Characters Before Output

Another option to mitigate this issue is to escape all special characters. To this, the url.QueryEscape function could be used:

import "net/url"
safePath := url.QueryEscape(r.URL.Path) // Encodes all unsafe characters

It’s recommended that you look for the solution that best fits your application.

Conclusion

This research demonstrated how Go’s automatic MIME type detection can lead to unintended file downloads, which:

  • Can be leveraged in social engineering attacks.
  • Can be used for phishing or file confusion exploits.
  • Can be mitigated by explicitly setting the Content-Type or escaping special characters before the output.

Even though the nature of this issue is quite unlikely, an understanding of these nuances may be useful to some developers to write more secure Go applications and prevent similar issues.

Learn more How MIME Type Handling in Go Can Lead to Unintended File Downloads

Leave a Reply