聊天缩略图绕过 s3_cdn_url 并使用原始 S3 存储桶 URL

我最近配置了 Cloudflare R2 上传存储桶,结果聊天缩略图显示异常。于是我深入排查并快速修复了配置,随后发现了这个主题:https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204。无论如何,我查看了其他 S3 上传存储桶的配置,发现这个问题并非 Cloudflare 特有。


描述

当为上传配置了外部 S3 或兼容的对象存储时,聊天缩略图会绕过 CDN,直接从存储桶 URL 加载。

对于像 Cloudflare R2 这样需要安全配置的外部 S3 兼容存储桶,聊天缩略图会损坏且无法显示。

根本问题在于聊天序列化器未能将 s3_cdn_url 设置应用于缩略图。图像未通过配置的 CDN 进行路由,而是将原始的 S3 存储桶内部 URL 直接泄露到了浏览器负载中。

复现步骤

在 Meta 及其他使用 S3 上传存储桶的站点上均可复现此问题:

  1. 在聊天或频道中发布一张图片
  2. 在控制台检查缩略图图片的 URL
  3. 点击图片以获取更大的原图,并检查其 URL
  4. 将其与缩略图 URL 进行对比

以下是来自 Meta 聊天的示例:

缩略图 URL:来自存储桶

https://cdck-file-uploads-global.s3.dualstack.us-west-2.amazonaws.com/meta/optimized/4X/4/7/9/479815360e0e6e0cd9f4ba565891776e84aea532_2_375x500.jpeg

原图 URL:通过 CDN

https://global.discourse-cdn.com/meta/original/4X/4/7/9/479815360e0e6e0cd9f4ba565891776e84aea532.jpeg

在控制台中,缩略图的 HTML 代码 <img... 包含 data-large-src(CloudFront CDN URL)和 src(AWS 存储桶 URL)。

截图

影响:

  • 对于像 Cloudflare R2 这样默认安全且阻止未授权访问原始存储桶端点的 S3 兼容存储,聊天缩略图(优化后的)会损坏。
  • 对于允许访问原始存储桶端点的 AWS 及其他 S3 兼容对象存储桶,会导致带宽泄露,因为聊天完全绕过了 CDN;这将导致为所有聊天缩略图流量支付直接的 S3 出站费用。
  • 基础设施泄露:原始后端存储 URL(包括内部存储桶名称,有时甚至包括账户 ID)正被暴露在客户端 JSON 负载中。

拉取请求(PR):

我提交了一个修复该问题的 PR:

https://github.com/discourse/discourse/pull/40419

看起来 Sam 添加了 getURLWithCDN 到聊天编辑器预览中——不过,我认为它并没有应用到聊天流中?

我想知道编辑器的修复是否在某些 S3 配置下也失败了,因为 getURLWithCDN 在协议不匹配(//https://)时会崩溃?无论如何,上述 PR 通过为聊天流添加包装器并使其与协议无关,扩展了 Sam 的工作。

临时解决方案:

在我意识到这不仅仅是 Cloudflare 的问题之前,我制作了一个轻量级的主题组件。它在浏览器尝试下载之前,拦截聊天 DOM 中的原始 S3 域名,并将其替换为正确的 CDN 域名。这正确地路由了流量并堵住了带宽泄露的漏洞。我对其进行了适配,使其适用于任何 S3 兼容的对象存储。只需两个设置——「原始 S3 存储桶 URL」和「S3 CDN URL」。

https://github.com/Lillinator/chat-s3-thumbnails-fix

(不知道为什么 GitHub 的自动链接在这里失效了) 现已修复

5 个赞

可能与网站迁移有关……我刚重新烘焙了内容。

2 个赞

你好,PR 已经合并了吗?

谢谢

2 个赞

现在它正等待人类团队成员审核,我的好友 discourse-triage-bot 已经修复了一些测试 :slight_smile:

https://github.com/discourse/discourse/pull/40419#issuecomment-4683409099

5 个赞

看起来已经合并了。

4 个赞

根据作者请求开启

对于添加了自定义表情的站点,修复合并后,聊天中的表情现在会失效。

与标准帖子图片不同(可以通过运行 rake posts:rebake 修复),聊天自定义表情是通过 /site.json 动态传递给前端的。

如果你的数据库中包含缺少协议的 S3 URL(例如 //bucket.endpoint...),或者使用了虚拟主机风格域名且该域名与你的 app.yml 环境变量不完全匹配,Discourse 的内部 CDN 替换器会静默失败。原始的存储桶 URL 会被传递给浏览器,从而导致聊天中的自定义表情失效。

如何修复:

要永久修复此问题,你需要在数据库中强制将原始存储桶 URL 映射到你的 CDN URL,然后清除站点缓存,以便 /site.json 重新生成。

1. 进入容器:

SSH 登录到你的服务器并进入 Discourse 容器(通常是 app,如果你使用双容器设置,则是 web_only)。

cd /var/discourse
./launcher enter app

2. 重新映射 URL:

运行内置的 Discourse remap 工具。你应该运行两次,以捕获 https:// 变体以及迁移脚本有时会遗留的无协议 // 变体。

将占位符替换为你的实际原始存储桶 URL 和实际 CDN URL:

# 修复标准的 https:// URL
discourse remap "https://<your-bucket>.<your-endpoint>.com" "https://cdn.your-domain.com"

# 修复无协议的 // URL(这通常是导致自定义表情失效的原因)
discourse remap "//<your-bucket>.<your-endpoint>.com" "https://cdn.your-domain.com"

3. 清除缓存

由于 /site.json 被高度缓存,你必须清除 Rails 缓存以强制论坛提供新的 URL:

打开 Rails 控制台:

rails c

运行以下命令:

Rails.cache.clear
Site.clear_cache
exit

4. 刷新

硬刷新你的浏览器(如果仍在使用,请禁用主题组件的临时解决方案)。聊天中的自定义表情现在应该已修复,并通过 CDN 正常加载。

3 个赞

嗨,Lilly,非常感谢你出色的工作。经过两次重新映射后,我之前的上传文件和缩略图现在都能正常显示了。在此之前,就连我的 “/admin/config/customize/themes” 图片都无法加载。现在一切都恢复正常了,真不错。

谢谢!

1 个赞