Writeups

Go语言项目容器化导致的Server-Side MIME Sniff

answerdev/answer 是基于go语言编写的一个问答平台(类似于知乎),前几周在审计该项目时,发现该系统的图片上传功能点存在一个有趣的漏洞

该系统的图片上传功能点的工作原理大致如下:

  • step1. 用户上传图片文件,将文件存储在本地文件系统中
  • step2. 当需要访问图片时,使用gin框架提供的静态资源服务器将用户上传的图片文件作为静态资源返回给用户

internal/router/static_router.go

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

为了防止用户上传恶意文件,该系统的文件上传功能设置了一个后缀白名单, 用户只能上传白名单中后缀的文件

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,
	}

起初,我在本地搭建了该项目,对文件上传功能和静态资源服务的进行了测试:我尝试上传文件内容为<script>alert(1)</script> , 文件后缀为 ‘.bmp’ ‘.tif’ ‘.tiff’ 的文件,然后通过静态资源服务访问这些文件,我发现返回的响应报文中Content-Type 分别为 image/bmp image/tiff image/tiff ,这些MIME都是正常的图片类型,无法使得浏览器将响应报文中的内容作为html来解析,从而造成XSS 。

但是,神奇的是当我使用 answer 官方提供的docker镜像来搭建answer时:

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

再次上传同样的文件,然后访问文件,发现返回的http响应报文的Content-Type 竟然变为了 text/html !

step1. 上传图片文件

step2. 访问文件

罪魁祸首 mime 标准库

之所以go实现的静态资源服务器会出现这种将 bmp/tif/tiff 等后缀的图片文件的Content-Type设置为text/html的情况,是因为go语言的mime标准库的实现有问题。

go语言实现的静态资源服务器在返回文件时,大致会执行以下步骤:

  • step1. 调用mime.TypeByExtension()函数来根据文件后缀获取对应的Content-Type
  • step2. 如果mime.TypeByExtension()函数返回的Content-Type为空字符串,则会进行Server-Side MIME Sniff ,即根据文件内容来判断对应的Content-Type

而mime.TypeByExtension()函数的实现实际上是依赖于外部的mime.types文件的,其本身所维护的文件后缀与 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.

在容器化过程中,为了追求最小化攻击面、更小的镜像体积,往往会使用alpine系列镜像,而该系列镜像中并没有以上所列的mime.types文件:

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

例如: /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)
	}
}

本地运行:

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

使用golang官方镜像: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

话句话说,如果你的静态资源服务器是基于go语言的mime标准库来实现的,那么你的静态资源服务器在使用alpine镜像容器化之后很可能会出现这种将bmp/tif/tiff等后缀的图片文件的Content-Types识别为text/html的情况,进而导致存储型XSS漏洞。

总结

该漏洞有趣的点在于,它揭示了安全问题与环境的关系: 测试环境下没有安全问题,不代表生产环境下也没有安全问题。

容器化在追求最小化攻击面的同时也引入了新的攻击面,go语言mime标准库的问题便是典型的例子。

依赖外部文件的标准库实现 -----> 容器化----->外部文件缺失----> 标准库功能出现安全问题

修复方案

go语言 mime标准库维护的内置的mime类型映射关系非常有限: 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",
}

方案一: 打包镜像时将如下mime.types文件拷贝到容器中:

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

方案二: 在实现图片上传功能时,不要将mime标准库内置表之外的后缀类型添加到白名单中,例如: .bmp .ico .tif .tiff

方案三: 使用nginx作为静态资源服务器

进一步的研究

  • 其他语言的静态资源服务器容器化后是否存在同样的问题?
  • 容器化是否会引入其他新的安全问题?

Keep In Touch

如果你有任何问题或者好的研究方向,欢迎联系我: hdrw1024@gmail.com

微信公众号: