首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >COS 浏览器上传跨域排查:分清预检失败、403 与 ETag 不可读

COS 浏览器上传跨域排查:分清预检失败、403 与 ETag 不可读

原创
作者头像
一只特立独行的兔先生
发布于 2026-10-19 12:44:10
发布于 2026-10-19 12:44:10
330
举报

先给结论:CORS 放行不等于对象授权

浏览器向 COS 上传文件失败,不要先把存储桶改成公有读写。CORS 决定浏览器是否允许网页发起跨域请求、读取响应;签名和权限策略决定请求能否操作对象。这是两道独立检查,放开其中一道不能替代另一道。

本文以网页通过预签名 URL 执行单次 PUT 为例,把故障拆成预检、实际上传、响应头读取三个阶段。下面的域名、桶名和对象路径均为示例,需替换成自己的测试资源;不涉及分块上传。

一、先在浏览器确认失败发生在哪一步

打开开发者工具 Network 面板,保留日志后重新上传,按对象路径过滤请求。

  • 只有 OPTIONS,未出现 PUT:先检查预检响应以及浏览器控制台报错。
  • OPTIONS 通过,PUT 返回 403:检查签名、过期时间、对象路径和授权范围;同时检查实际响应的跨域头。
  • PUT 成功,但代码拿不到 ETag:检查响应头暴露配置,不要重复上传。

不要只看 JavaScript 的 Failed to fetch。这个错误本身不能区分网络中断和跨域拦截。浏览器即使不允许脚本读取响应,服务端也可能已经执行了请求;重试之前应核对对象是否存在,避免重复副作用。

二、根据真实请求配置最小规则

先记下三个值:网页的 Origin、实际请求方法、预检里的 Access-Control-Request-Headers。

Origin 由协议、主机和端口组成,不包含页面路径。https://app.example.com 与 http://app.example.com 不同,本地开发端口变化也会改变 Origin。

COS 的 PUT Bucket cors API 使用以下 XML 字段。这个示例仅展示一条规则的形态,不是要求直接覆盖已有配置:

代码语言:xml
复制
<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>https://app.example.com</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>content-type</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <MaxAgeSeconds>600</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>

AllowedMethod 填实际操作 PUT,不是因为看到预检就填 OPTIONS。AllowedHeader 对应浏览器准备发送的请求头;如果 SDK 还发送其他头,应根据实际预检补齐,不能照抄上面的一项。

ExposeHeader 则控制脚本能读取哪些非默认暴露的响应头。允许发送 content-type 与允许读取 ETag 是两个方向,不要混用。

这里的 600 是示例预检缓存秒数,不是推荐所有业务照搬的固定值。调整规则前先读取、备份现有配置,保留其他站点依赖的规则,避免修好一个前端却破坏另一个。

三、用 curl 检查预检响应

先选择一个无敏感信息的测试对象地址:

代码语言:bash
复制
curl -i -X OPTIONS \
  'https://examplebucket-1250000000.cos.ap-guangzhou.myqcloud.com/cors-probe.txt' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: content-type'

核对响应里的 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 是否覆盖本次请求。不能仅凭状态码为 2xx 就判断浏览器一定会放行。

curl 不执行浏览器的同源策略,因此“curl 能上传”不能证明网页跨域配置正确。这个命令用于观察服务端如何响应预检,最终仍要回到真实网页验证。

四、验证实际上传与响应读取

在允许来源的测试网页执行以下代码,signedUrl 应由自己的可信后端生成,且只允许写入指定测试对象。不要把长期密钥放进页面。

代码语言:javascript
复制
async function uploadProbe(signedUrl) {
  const response = await fetch(signedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'text/plain' },
    body: 'cors-probe\n',
    credentials: 'omit'
  });
  if (!response.ok) {
    throw new Error(`上传失败:HTTP ${response.status}`);
  }
  return { status: response.status, etag: response.headers.get('ETag') };
}

如果生成签名时把 Content-Type 纳入签名,上传时必须使用一致的值。请求方法、对象路径或已签名请求头发生变化,都应从签名链路排查,而不是继续扩大 CORS 规则。

若返回成功但 etag 为 null,先在 Network 面板确认服务端确实返回了 ETag,再检查 ExposeHeader。ETag 在这里用于验证响应头可读,不应一概当作文件 MD5。

五、修复后保留一组反向检查

从允许的来源上传一个小文件,确认 PUT 成功、脚本能读取 ETag,并在对象列表确认文件。再从未授权来源测试,确认浏览器不会向脚本开放跨域访问。后者不代表非浏览器客户端也无法访问,真正的安全边界仍是对象权限与签名。

修改规则后还要考虑预检缓存。清理相关缓存或换一个干净的浏览器会话复测,记录实际 Origin、请求头和响应头;不要为了消除报错把来源永久设成通配符,也不要把私有桶改为公有写。

参考资料

以上为排障步骤与示例,实际效果需在自己的桶和网页来源上验证。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先给结论:CORS 放行不等于对象授权
  • 一、先在浏览器确认失败发生在哪一步
  • 二、根据真实请求配置最小规则
  • 三、用 curl 检查预检响应
  • 四、验证实际上传与响应读取
  • 五、修复后保留一组反向检查
  • 参考资料
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档