0x0 背景介绍
在podinfo 6.9.0 及之前版本中,未经过身份验证的攻击者可以通过向/store端点发送伪造的 POST请求上传任意文件。由于应用在未启用严格的Content-Security-Policy(CSP)或未进行充分的Content-Type验证的情况下直接渲染上传内容,从而导致存储型跨站脚本攻击(XSS)。
0x1 环境搭建
1.1、Ubuntu24+Docker搭建配置
bash
#1、创建专属项目
mkdir podinfo-rce && cd podinfo-rce
#2、拉取环境
git clone https://github.com/stefanprodan/podinfo.git
git checkout 6.9.0
#3、拉取镜像
docker build -t podinfo:6.9.0-local .
#4、挂载 data 目录,赋权,我一开始没创建但是上传失败
mkdir -p ~/podinfo-data
chmod 777 ~/podinfo-data # 简化权限(测试环境可用)
#5、启动项目
docker run -d \
--name podinfo-690 \
-p 9898:9898 \
-v ~/podinfo-data:/data \
podinfo:6.9.0-local
0x2 漏洞复现
2.1、YML复现
YML验证
bash
https://github.com/Kai-One001/cve-/blob/main/Podinfo_Upload_CVE_2025_70849.yml

- 验证是否成功

2.2、复现流量特征 (PCAP)
- 无认证请求接口,流量整体明显,还是方便监控的


0x3 漏洞原理分析
我们需要先找下入口文件,go语言程序的起点一般都是 main() 函数,在podinfo 中,主程序就在:cmd/podinfo/main.go
3.1、程序入口
go
import (
// ... 其他包
"github.com/stefanprodan/podinfo/pkg/api/http"
)
func main() {
//具体调用分析// 1. 加载 HTTP 服务器配置
var srvCfg http.Config
if err := viper.Unmarshal(&srvCfg); err != nil {
logger.Panic("config unmarshal failed", zap.Error(err))
}
// 2. 创建 HTTP 服务器实例
srv, _ := http.NewServer(&srvCfg, logger)
httpServer, httpsServer, healthy, ready := srv.ListenAndServe()
// 3. 启动服务器
httpServer, httpsServer, healthy, ready := srv.ListenAndServe()
}
HTTP服务器的实现位于pkg/api/http/中,接着往下找
3.2、上传接口 /store:任意内容写入磁盘
- 文件路径:
pkg/api/http/store.go - 函数:
storeWriteHandler
go
func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) {
_, span := s.tracer.Start(r.Context(), "storeWriteHandler")
defer span.End()
body, err := io.ReadAll(r.Body)
if err != nil {
s.ErrorResponse(w, r, span, "reading the request body failed", http.StatusBadRequest)
return
}
hash := hash(string(body))
err = os.WriteFile(path.Join(s.config.DataPath, hash), body, 0644)
if err != nil {
s.logger.Warn("writing file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash)))
s.ErrorResponse(w, r, span, "writing file failed", http.StatusInternalServerError)
return
}
s.JSONResponseCode(w, r, map[string]string{"hash": hash}, http.StatusAccepted)
}
| 风险点 | 说明 |
|---|---|
| 未认证访问 | 无任何身份验证或授权检查,只要网络可达用户都可以调用 |
| 任意内容上传 | 使用 io.ReadAll(r.Body) 读取原始请求体 未校验 Content-Type(可为 text/html、application/javascript 等) 未限制内容长度、格式、字符集 |
| 直接落盘 | 调用 os.WriteFile(path.Join(s.config.DataPath, hash), body, 0644) 原样写入磁盘 |
| 路径可控 | DataPath 默认为 /data(见 main.go 中 fs.String("data-path", "/data", ...)),能通过命令行参数或环境变量 PODINFO_DATA_PATH 修改 |
| 文件名机制 | 文件名为 SHA1(body),内容完全由访问者控制 |
| 文件权限 | 权限设为 0644,运行用户可读写,其他用户可读 |
3.3、下载接口 /store/{hash}:不安全内容回显
- 文件位置:
pkg/api/http/store.go - 函数:
storeReadHandler
go
func (s *Server) storeReadHandler(w http.ResponseWriter, r *http.Request) {
_, span := s.tracer.Start(r.Context(), "storeReadHandler")
defer span.End()
hash := mux.Vars(r)["hash"]
content, err := os.ReadFile(path.Join(s.config.DataPath, hash))
if err != nil {
s.logger.Warn("reading file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash)))
s.ErrorResponse(w, r, span, "reading file failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
w.Write([]byte(content))
}
| 风险点 | 说明 |
|---|---|
| 内容直接原样回显 | 只做os.ReadFile读取,直接w.Write(content)输出;没有对内容进行任何过滤、转义或安全封装 |
Content-Type 缺失 |
代码中未显式设置 Content-Type Swagger 注解 @Produce plain 仅为文档,不影响运行时行为 Go HTTP服务会基于前 512 字节进行 MIME 嗅探,可能将HTML/JS识别为可执行类型 |
| 关键安全头缺失 | 未设置 X-Content-Type-Options: nosniff → 无法阻止浏览器覆盖 MIME 类型 未设置 Content-Security-Policy → 无法限制脚本执行- 未设置 X-Frame-Options → 存在点击劫持风险 |
| 路径遍历防护不足 | 虽使用 SHA1 作为文件名,但未校验 hash 参数是否为 40 位十六进制字符串 缺少对路径拼接的安全加固(尽管当前因命名机制暂无实际穿越风险) |
| 与上传接口形成完整攻击链 | 恶意内容写入后可直接通过此接口访问 |
3.4、路由与中间件注册机制
在 pkg/api/http/server.go中注册了文件存取相关的HTTP路由:
- 路由注册代码:
go
func (s *Server) registerHandlers() {
// ... 其他路由
s.router.HandleFunc("/store", s.storeWriteHandler).Methods("POST", "PUT")
s.router.HandleFunc("/store/{hash}", s.storeReadHandler).Methods("GET").Name("store")
// ... 其他路由
}
- 中间件注册代码:
go
func (s *Server) registerMiddlewares() {
prom := NewPrometheusMiddleware()
s.router.Use(prom.Handler)
otel := NewOpenTelemetryMiddleware()
s.router.Use(otel)
httpLogger := NewLoggingMiddleware(s.logger)
s.router.Use(httpLogger.Handler)
s.router.Use(versionMiddleware)
if s.config.RandomDelay {
randomDelayer := NewRandomDelayMiddleware(s.config.RandomDelayMin, s.config.RandomDelayMax, s.config.RandomDelayUnit)
s.router.Use(randomDelayer.Handler)
}
if s.config.RandomError {
s.router.Use(randomErrorMiddleware)
}
}
- 服务器启动流程:
go
func (s *Server) ListenAndServe() (*http.Server, *http.Server, *int32, *int32) {
// ...
s.registerHandlers() // 先注册路由
s.registerMiddlewares() // 后注册中间件(全局应用)
// ...
}
-
/store:POST/PUT,无任何鉴权中间件要求 -
/store/{hash}:GET,无鉴权、不做参数过滤 -
全局应用 :所有中间件通过
Use()应用到所有路由,store路由也没有例外
3.5、前端 UI 与 /store 的关系
ui/vue.html仅展示前端界面,并调用部分API:
- 文件位置:
pkg\api\http\index.go
go
package http
import (
"html/template"
"net/http"
"path"
)
// Index godoc
// @Summary Index
// @Description renders podinfo UI
// @Tags HTTP API
// @Produce html
// @Router / [get]
// @Success 200 {string} string "OK"
func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
_, span := s.tracer.Start(r.Context(), "indexHandler")
defer span.End()
tmpl, err := template.New("vue.html").ParseFiles(path.Join(s.config.UIPath, "vue.html"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(path.Join(s.config.UIPath, "vue.html") + err.Error()))
return
}
data := struct {
Title string
Logo string
}{
Title: s.config.Hostname,
Logo: s.config.UILogo,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, path.Join(s.config.UIPath, "vue.html")+err.Error(), http.StatusInternalServerError)
}
}
ui/vue.html中的JS主要与/api/info和/api/echo交互:
html
new Vue({
...
methods: {
getInfo: function() {
const xhr = new XMLHttpRequest();
let self = this;
xhr.open('GET', "api/info")
xhr.onload = function () {
data = JSON.parse(xhr.responseText)
...
self.info = data
self.cuddleStyle = {
backgroundColor: data.color
}
self.info.logo = data.logo
document.title = data.hostname
...
}
...
},
postBackend: function() {
var self = this
fetch("api/echo", {
method: 'post',
headers: {
"Content-type": "application/json; charset=UTF-8",
"X-APP": Math.random(),
},
body: JSON.stringify({random: Math.random()})
})
...
}
}
})
-
官方
UI本身没有直接使用/store内容 -
存储型
XSS更类似于在同域下托管恶意页面/脚本,然后通过外部引导(链接、iframe/script引用)让用户浏览器加载
0x4 修复建议
修复方案
-
升级最新版本: 尽快升级至官方发布的最新修复版本,具体参考: GitHub Release
-
临时防护措施:
加强上传校验 :限制/store端点的访问权限(加认证+校验MIME类型和扩展名校验)
强制设置安全响应头 :添加X-Content-Type-Options、Content-Security-Policy等,禁止内联脚本执行
日志监控与告警 :对/store端点的异常请求进行实时监控,拦截恶意上传行为
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。