Writeups

Server-Side MIME Sniff resulting from Go language project containerization

answerdev/answer is a question and answer platform which is based on the go language, a few weeks ago I audit of the project, found there is an interesting vulnerability in the image upload function

The working principle of the image uploading function of the system is roughly as follows:

  • step1. A user uploads an image file and stores the file in the local file system
  • step2. When images need to be accessed, use the static resource server provided by gin framework to return the uploaded image files to the user as static resources

internal/router/static_router.go

// RegisterStaticRouter register static api router
func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) {
	r.Static("/uploads", a.serviceConfig.UploadPath)
}

To prevent users from uploading malicious files, the file upload function of the system sets a suffix whitelist. Users can upload files with the suffix in the whitelist

internal/service/uploader/upload.go

	FormatExts = map[string]imaging.Format{
		".jpg":  imaging.JPEG,
		".jpeg": imaging.JPEG,
		".png":  imaging.PNG,
		".gif":  imaging.GIF,
		".tif":  imaging.TIFF,
		".tiff": imaging.TIFF,
		".bmp":  imaging.BMP,
	}

At first, I built the project locally and tested the file upload function and static resource service: I tried to upload <script>alert(1)</script> with the suffix .bmp .tif .tiff and access the files through static resource services. I found that the content-types in the returned response messages were image/bmp, image/tiff, image/tiff respectively. These MIME types were normal image types, which could not make the browser parse the contents in the response messages as html

However, what is amazing is that when I use the docker image provided by answer official to build answer:

docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest

Upload the same file again, then access the file, and find that the Content-Type of the returned http response message is changed to text/html!

step1. Upload a picture file

step2. Access the file

The culprit is the mime standard library

The reason why the Content-Type of bmp/tif/tiff image files is set to text/html in the static resource server implemented by go is that the mime standard library of go language is flawed.

The static resource server implemented by the go language generally performs the following steps when it returns a file:

  • step1. Call the mime.TypeByExtension() function to get the Content-Type based on the file suffix
  • step2. If the Content-type returned by the mime.TypeByExtension() function is an empty string, a Server-Side MIME Sniff is implemented to identify the Content-type based on the file Content

The implementation of the mime.TypeByExtension() function actually depends on the external mime.types file, which itself maintains a very limited mapping between file suffixes and Content-Type

/usr/local/go/src/mime/type.go

// TypeByExtension returns the MIME type associated with the file extension ext.
// The extension ext should begin with a leading dot, as in ".html".
// When ext has no associated type, TypeByExtension returns "".
//
// Extensions are looked up first case-sensitively, then case-insensitively.
//
// The built-in table is small but on unix it is augmented by the local
// system's MIME-info database or mime.types file(s) if available under one or
// more of these names:
//
//   /usr/local/share/mime/globs2
//   /usr/share/mime/globs2
//   /etc/mime.types
//   /etc/apache2/mime.types
//   /etc/apache/mime.types
//
// On Windows, MIME types are extracted from the registry.

In containerization, the alpine series of images is often used in order to minimize the attack surface and reduce the image size. The mime.types files listed above are not included in the Alpine series of images:

/usr/local/share/mime/globs2
/usr/share/mime/globs2
/etc/mime.types
/etc/apache2/mime.types
/etc/apache/mime.types

for example:

/Users/rickshang/Code/SecurityResearch/InTheLab/content_type_lab/golang/fuzzer/main.go

package main

import (
	"fmt"
	"mime"
)

func main() {
	exts := []string{".bmp", ".gif", ".jpeg", ".jpg", ".png", ".svg", "ico", ".tif", ".tiff", ".webp"}
	for _, ext := range exts {
		content_type := mime.TypeByExtension(ext)
		fmt.Printf("ext:%s content_type:%+v\n", ext, content_type)
	}
}

Run locally:

go run main.go

ext:.bmp content_type:image/bmp
ext:.gif content_type:image/gif
ext:.jpeg content_type:image/jpeg
ext:.jpg content_type:image/jpeg
ext:.png content_type:image/png
ext:.svg content_type:image/svg+xml
ext:.ico content_type:image/x-icon
ext:.tif content_type:image/tiff
ext:.tiff content_type:image/tiff
ext:.webp content_type:image/webp

Run with the golang official image: golang:1.19-alpine

docker run -it --rm -v /Users/rickshang/Code/SecurityResearch/InTheLab/content_type_lab/golang/fuzzer/main.go:/code/main.go -w /code golang:1.19-alpine go run main.go

ext:.bmp content_type:
ext:.gif content_type:image/gif
ext:.jpeg content_type:image/jpeg
ext:.jpg content_type:image/jpeg
ext:.png content_type:image/png
ext:.svg content_type:image/svg+xml
ext:.ico content_type:
ext:.tif content_type:
ext:.tiff content_type:
ext:.webp content_type:image/webp

In other words, if your static resource server is implemented based on go’s mime standard library, When your static resource server uses alpine image containerization, it is likely that Content-Types of bmp/tif/tiff image files will be identified as text/html, leading to stored XSS vulnerabilities.

Summary

What’s interesting about this vulnerability is that it reveals how security issues relate to the environment: just because there are no security issues in test environments doesn’t mean there are no security issues in production environments.

Containerization in pursuit of minimizing attack surfaces also introduces new attack surfaces, the go language mime standard library is a typical example.

A library implementation that relies on external files -----> containerization----->Missing external file----> a security issue is born

Mitigation measures

The go Language mime standard library maintains a very limited built-in mime type mapping: go/src/mime/type.go

var builtinTypesLower = map[string]string{
	".avif": "image/avif",
	".css":  "text/css; charset=utf-8",
	".gif":  "image/gif",
	".htm":  "text/html; charset=utf-8",
	".html": "text/html; charset=utf-8",
	".jpeg": "image/jpeg",
	".jpg":  "image/jpeg",
	".js":   "text/javascript; charset=utf-8",
	".json": "application/json",
	".mjs":  "text/javascript; charset=utf-8",
	".pdf":  "application/pdf",
	".png":  "image/png",
	".svg":  "image/svg+xml",
	".wasm": "application/wasm",
	".webp": "image/webp",
	".xml":  "text/xml; charset=utf-8",
}

Solution 1: When packing the image copy the following mime.types file into the container:

/usr/local/share/mime/globs2
/usr/share/mime/globs2
/etc/mime.types
/etc/apache2/mime.types
/etc/apache/mime.types

Solution 2: Do not add suffixes other than the mime library builtin table to the whitelist when implementing the image upload function, for example,.bmp.ico.tif.tiff

Solution 3: Use nginx as a static resource server

Further research

  • Do static resource servers in other languages have the same problem after containerization?
  • Does containerization introduce any new security issues?

Keep in touch

If you have any questions or good research direction, please feel free to contact me: