对象存储被DDoS打穿?预签名URL和分片上传接口才是真正的后门

别急着给对象存储套高防CDN。我见过太多团队把OSS/MinIO藏在高防后面,结果攻击者不碰你的回源IP,也不打SYN Flood,就靠存储桶合法的URL签名和分片上传接口,用10Mbps的小流量把你的元数据服务打瘫痪。今天不聊高防架构,只讲那些藏在S3兼容协议里的盲区。

一、你以为藏住了源站,其实预签名URL把桶地址写在了CDN日志里

很多人的方案是“CDN回源到对象存储”,回源走内网IP,CDN节点用预签名URL去桶里拉数据。问题在于:这个带签名参数的URL会在CDN访问日志里明文留痕。 攻击者只要能读到你某一次的CDN日志(比如日志服务未鉴权、合作伙伴泄漏),就能拿到完整签名URL。S3的预签名URL逻辑是:签名只覆盖Host和部分Header,X-Amz-Signature绑定的资源路径是固定不变的。拿到一个合法签名,意味着攻击者可以无限次调用GetObject去刷流量——直到签名过期。 更坑的是,很多对象存储默认签名有效期是7天。你在高防CDN上把回源超时调到60秒,CPU满载时回源排队超过这个时间,签名已经验过了但数据没吐完,CDN直接断连重试,重试带着旧签名+新时间戳(如果你用SDK自动刷新),源站以为你是合法的,继续放行。
# 查一下你CDN回源时实际拿到的URL(以MinIO为例)
mc stat myminio/bucket/reports/2025-04.pdf --json | jq .url
# 你会发现URL里带了X-Amz-Expires=604800
# 这个值=3600*24*7,默认签名有效期就是7天
避坑点:预签名URL的有效期在CDN回源场景下不是越长越好。如果你的CDN节点回源频率固定,签个1小时就够。把X-Amz-Expires改到3600,即使日志泄漏,攻击者的利用窗口也缩短到1/168。

二、比SYN Flood更阴间的打法:用ListObjects刷爆元数据服务

能挡100Gbps SYN Flood的高防,挡不住5Mbps的GET ListObjects。这个接口的复杂度是O(N),攻击者只要用50个线程持续调用:
# 攻击者视角的刷接口命令(你看不到,但要有心理准备)
# while true; do curl -s "https://your-s3/v2?list-type=2&continuation-token=XXXX" -H "Authorization: Bearer LEAKED_TOKEN" > /dev/null; done

# 更狠的是带prefix参数递归遍历,让对象存储做深度目录遍历
curl -s "https://your-s3/v2?prefix=dir1/dir2/dir3/&list-type=2&max-keys=1000"
对象存储的性能瓶颈不在数据面,在元数据面。一张1M的图片读出来只要几毫秒,但列出10万个对象做一次排序需要几百毫秒CPU。攻击者拿你泄漏的合法token,反复调用ListObjectsV2,你的MinIO进程的GC线程直接拉满。 防护方案:对象存储前面加一层轻量Nginx做API网关,对敏感接口做限流:
# /etc/nginx/conf.d/s3-gateway.conf
limit_req_zone $binary_remote_addr zone=s3_list:10m rate=5r/s;
server {
    listen 8080;
    location ~* ^/bucket/_list {
        # 只允许已知的CDN回源IP段执行List操作
        allow 183.xx.xx.0/24; # 你的高防CDN回源网段
        deny all;
        limit_req zone=s3_list burst=10 nodelay;
        proxy_pass http://127.0.0.1:9000;
    }
}
同时把bucket policy里的ListBucket权限收死,只给单独建一个只读子账号给CDN回源用,永远不要用admin key。

三、分片上传的“孤儿Part”能塞满你的存储池

S3兼容协议里,CompleteMultipartUpload和AbortMultipartUpload是两个独立的接口。真实攻击案例:攻击者用自动化脚本,每10秒发一次CreateMultipartUpload,不传完Part也不调用Abort,只留一堆UploadId。
# 攻击者脚本核心逻辑(脱敏简化版)
for i in $(seq 1 10000); do
  curl -s -X POST "https://your-s3/bucket/${i}.tmp?uploads=" -H "Authorization: Bearer LEAKED_TOKEN"
  # 拿到UploadId后直接丢弃,既不传文件也不complete
done
# 每个UploadId默认保留3-7天(MinIO默认7天),占用元数据内存+dirty page
这种攻击流量极小(一个HTTP POST请求也就2KB),但每个UploadId在存储端会占一个inode,即使没传Part,元数据里也会有记录。当你磁盘上有10万个孤儿Part,重启MinIO时扫描元数据的时间会从秒级变分钟级。 止损命令:
# MinIO控制台 -> Bucket -> 生命周期规则
mc ilm rule add myminio/tmp --expire-days 1 --prefix "/" --noncurrent-expire-days 1
# 或者用mc管理命令手动清:
for u in $(mc ls --incomplete myminio/bucket | awk '{print $2}'); do
  mc rm --incomplete --force "myminio/bucket/$u"
done
同时把CreateMultipartUpload接口的IP白名单设成只有你的上传服务所在网段。轻云互联的高防云服务器上就可以做这个策略:先用安全组限制只能从指定IP发起POST uploads请求,再用Nginx的$request_method$request_uri做二次拦截。

四、小流量攻击的排错实操:tcpdump+API日志钩子

如果你怀疑有人在打你的对象存储,不要先看带宽,先看API请求日志。S3兼容服务默认都带审计日志,重点查两个字段:
# MinIO的API日志,重点看Action名和RequesterIP的分布
journalctl -u minio | grep "ListBucket" | tail -n 100
# 输出里的RemoteHost可能是你CDN节点的内网IP,也可能是公网IP
# 如果发现某个IP的ListBucket调用频率异常,立刻封禁

# 用tcpdump配合抓包,判断是不是恶意客户端在绕过CDN直接连对象存储
tcpdump -i eth0 'tcp port 9000' -c 500 -w s3.pcap
# 重点检查握手序列:直接连源站的攻击者,SYN包间隔均匀;而正常CDN回源是突发性的
真正确认攻击后,最快的止血方式是改对象存储的监听端口,并在安全组里只放行CDN回源网段。这个操作30秒生效,比重配防火墙黑名单更直接。

五、反向代理层的一行关键配置

所有对象存储服务都需要在前面加一层反向代理来做协议层防护。Nginx里加这几个参数,能过滤90%的低级扫描:
location / {
    # 拒绝不带任何Authorization头的请求(如果全部走CDN回源)
    if ($http_authorization = "") {
        return 403;
    }
    # 限制单连接请求速率,防止burst刷接口
    limit_rate 20m;
    # 动态黑名单——实时读取最近被识别为恶意IP的文件
    access_log /var/log/nginx/s3-access.log;
}
需要注意:if ($http_authorization = "")这种写法要放在server块的最前面,且不要和limit_req同时出现,否则Nginx会报变量冲突。真正的生产环境我会用OpenResty的Lua脚本做动态IP维度封禁,但那是另一个话题了。

六、最后一条经验

对象存储的DDoS防护,核心不是把带宽扛下来,而是让所有API请求都经过你能控制校验逻辑的节点。我现在用轻云互联的高防服务器做对象存储的入口网关,把MinIO放在内网,所有公网请求先过他们的BGP清洗,再走nginx的token校验和限流,才彻底解决小流量元数据攻击的问题。如果你用云厂商的对象存储(OSS/S3),至少开一下KMS加密和访问点限制,别让预签名URL裸奔在外。