PDF 由浏览器内置阅读器扩展(mhjfbmdgcfjbbpaeojofohoefgiehjai)
在独立扩展进程里渲染,不是普通页面上下文,拿不到父页的 window / DOM / cookie。
PDF 本身是原生文档、不是 HTML,没有能访问父页面的脚本环境。
服务端另有 X-Content-Type-Options: nosniff + MIME 按内容嗅探(media.php),
PDF 不会被当 HTML 执行。
若将来真要禁止 PDF 内嵌 JS,正确做法是换 pdf.js 自渲染,
而不是给 iframe 挂 sandbox —— 那会把整个 PDF 预览一起废掉。
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
现象
资源模块里打开 PDF 附件时,预览区显示「该页面已被 Chrome 屏蔽」(This page has been blocked by Chrome),
PDF 无法阅读。同一份 PDF 直接在标签页里打开是正常的,只有走站内预览层时被拦。
初步定位(待实测确认)
前端预览层给 PDF 用的
<iframe>挂了sandbox:assets/js/preview.jskind === 'pdf'分支,第 90 行附近el.setAttribute('sandbox', 'allow-same-origin allow-scripts')已知机理:Chromium 系浏览器在 iframe 带
sandbox时会禁用内置 PDF 阅读器插件(即便给了
allow-scripts+allow-same-origin),表现为「被 Chrome 屏蔽」。其他浏览器(Firefox / Safari)行为不同,所以只有 Chrome 系复现。
该行引入于 v24 第 2 轮(commit
8f80f3e),当时的注释意图是「PDF 是另一份文档,别让它拿到本页的 window」—— 属于安全考虑,但对 PDF 这种
纯文档场景是过度限制:浏览器内置 PDF 阅读器本身已在独立进程/沙箱里运行。
排除项(已查)
media.php的 CSP 只对image/svg+xml下发(第 256-259 行),PDF 不受影响。Content-Disposition在无dl参数时是inline,正确。.htaccess里没有X-Frame-Options/frame-ancestors。X-Content-Type-Options: nosniff与 PDF 渲染无关。⇒ 问题在前端 iframe 的 sandbox 属性,不在服务端响应头。
诉求
PDF 附件在预览层里能正常显示,并且不降低安全性:
去掉 iframe sandbox 后要确认内嵌文档不会拿到父页面的 window/权限。
验收标准
实测复现:确认是 iframe 的 sandbox,且加 escape 也救不了
环境:Chromium 153.0.8010.12(Playwright bundled),真实 headed 模式,本地 php -S 起站,
自造一张 592 字节的最小合法 PDF,五组对照同源加载。
抓到的原文
有 sandbox 的组,iframe 内部
inner_text就是:与用户描述完全一致。frame 列表里只剩
chrome-error://chromewebdata/。五组对照结果
chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html= 内置 PDF 阅读器)allow-same-origin allow-scripts(当前写法)allow-scripts+ allow-popups-to-escape-sandbox+ allow-forms allow-popups像素佐证(截图像素统计,判 PDF 阅读器的深灰工具栏):
🔴 关键结论:
allow-popups-to-escape-sandbox在这个版本上无效Stack Overflow 高赞答案建议加
allow-popups-to-escape-sandbox解决,实测在本版本不成立(D 组照样被屏蔽)。E 组把
allow-forms、allow-popups全加上也无效。⇒ 只要 iframe 上存在
sandbox属性,Chromium 就不给 PDF 插件权限。唯一可行做法是移除 sandbox 属性,而不是往里面加白名单。
安全性评估:移除 sandbox 是安全的,理由是
mhjfbmdgcfjbbpaeojofohoefgiehjai)本身就跑在独立扩展进程里,不是普通页面上下文,拿不到父页面的
window/ DOM / cookie。不是 HTML;它没有脚本执行环境去访问父页面(PDF 里的 JS 是另一套受限沙箱)。
X-Content-Type-Options: nosniff,且 MIME 由内容嗅探决定(
media.php第 22 行注释、第 255 行),PDF 不会被当成 HTML 执行。Content-Disposition: inline+nosniff(现状已满足);⇒ 移除
sandbox不引入新风险;保留它反而让功能完全不可用。修复方案(待实现)
assets/js/preview.js的kind === 'pdf'分支:el.setAttribute('sandbox', ...)两行;(这类改动最容易在安全 review 里被当成好主意)。
图片 / 视频分支不动,无语义变化。
修复:去掉 iframe 的 sandbox 属性
改动
assets/js/preview.js,kind === 'pdf'分支,1 处逻辑 + 一段防回归注释:只改这一个文件,
git diff --stat:1 file changed, 17 insertions(+), 2 deletions(-)。图片 / 视频分支没动。
验证(浏览器端到端,11/0)
新增
_tooling/_pdf_e2e.py:起本地 php -S → 真登录 → 走preview.js的真实逻辑打开 PDF。判据落在渲染层,不看 DOM 存在性:
反向验证:确认这组断言真能变红
把
sandbox那行注入回去,同一套断言立刻红 4 条,并抓到原文:恢复后重新全绿(11/0)。⇒ 红绿闭环成立,不是"恰好绿了"。
过程中抓到的两个真实问题(都已修)
_fb_site的preview.js是 10-05 的旧副本,md5 与源码不一致。第一次跑 e2e 报"仍有 sandbox"是夹具陈旧导致的假红,不是修复无效。
同步后才变绿。教训:改前端后跑探针前,必须先比对
_fb_site与源码的 md5。def pick_port(),被流水线 1d5 静态检查抓住(模块级调用早于 import ⇒ NameError,
py_compile抓不到)。已统一改为
from _fb_port import pick_port。部署
assets/js/preview.js由asset()统一带?v=<mtime+size 的 md5 前 8 位>版本号(
app/helpers.php:53),所以文件一变 URL 就变,浏览器缓存会自动失效,用户无需手动清缓存。该文件在
templates/layout.php里全局加载,_attachments.php的预览触发点被读书 / 笔记 / 旅程 / 观影多处复用 ⇒ 属共用件,按全量口径跑流水线。
已修复并部署
改动
只有一个文件、一处改动:
assets/js/preview.js的kind === 'pdf'分支,删掉
el.setAttribute('sandbox', 'allow-same-origin allow-scripts')。同时把 17 行实测结论写进代码注释(防止以后有人「为了安全」把它加回去):
五组同源对照已实测,只要 sandbox 属性存在就不给 PDF 插件权限,
allow-popups-to-escape-sandbox在本版本(Chromium 153)也救不了。git diff --stat:1 file changed, 17 insertions(+), 2 deletions(-)(原 sandbox 行引入于 v24 第 2 轮
8f80f3e)安全前提(为什么不加也安全)
PDF 由浏览器内置阅读器扩展(
mhjfbmdgcfjbbpaeojofohoefgiehjai)在独立扩展进程里渲染,不是普通页面上下文,拿不到父页的 window / DOM / cookie。
PDF 本身是原生文档、不是 HTML,没有能访问父页面的脚本环境。
服务端另有
X-Content-Type-Options: nosniff+ MIME 按内容嗅探(media.php),PDF 不会被当 HTML 执行。
若将来真要禁止 PDF 内嵌 JS,正确做法是换
pdf.js自渲染,而不是给 iframe 挂 sandbox —— 那会把整个 PDF 预览一起废掉。
验证
新增端到端套件
_tooling/_pdf_e2e.py,11 条断言全通过 0 失败,已登记进
familybook_push.sh(挂全路由标签,因为它服务全站)。关键判据落在渲染层,不看 DOM 存在性 —— 该 bug 屏蔽时 iframe 节点照样在,
DOM 断言全绿,只有看 frame 列表和 iframe 内文字才看得见:
iframe_sandbox is None(不再有 sandbox 属性)page.frames里没有chrome-error://chromewebdata/page.frames里有chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html(阅读器起来了)inner_text不含「屏蔽」字样<img>、没有 iframe(回归)⚠️ 该套件必须 headed 跑:headless 下 PDF 一律不渲染,会得到全假结论。
反向验证(falsification):把 sandbox 行注入回去 → 4 条断言转红(含「阅读器已启动」与「iframe 内没有屏蔽提示」);
恢复后回到 11/0。红绿闭环成立,说明这套断言有牙齿。
附带修掉的过程陷阱
跑验证时首轮报「sandbox 还在」,实际是
_fb_site夹具里的preview.js是 21 天前的旧副本(md5
70f599c6≠ 源码988b804f)。这是假红,不是修复无效。已把「同步夹具 + 失败时 raise 而不是静默返回」写进 e2e,避免同类假红/假绿。
关闭本单。线上验证请由你实测一次(资源模块打开 PDF 附件)。