'unsafe-inline' 写了也没用
主线位置:启动主线最后一步,
tauri://协议返回 HTML 时由set_csp拼出Content-Security-Policy头。拼的规则(哪些东西得到哈希、哪些得到 nonce、什么时候新建指令)见 tauri:// 协议与 CSP,那一页的动脑筋已经演示了onclick被拦;本页补上style属性这一半、三种改法和它们各自的代价。
flowchart TB
cfg["配置 script-src 含 unsafe-inline"] --> add{"tauri 追加了哈希或 nonce"}
add -->|"有 .js 或内联脚本"| ign["unsafe-inline 被忽略"]
add -->|否| keep["unsafe-inline 生效"]
ign --> attr["onclick 和 style 属性被拦"]
ign --> tag["带哈希或 nonce 的标签照常"]
实验 crate exp-tauri/pit3-inline:从模板 base 复制,窗口在 setup 里用 WebviewWindowBuilder 建,挂 on_web_resource_request 打印主页面响应的 CSP 头。dist/ 里一个 index.html、一个 app.js、一个 app.css,同一份代码换三份 app.security 配置构建。tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口,debug 构建,平台 M2 Max、macOS 27、rustc 1.99.0(WKWebView)。
现象
页面里新旧两种写法各放一份:
<style>.red { color: rgb(255, 0, 0); }</style>
</head><body>
<button id="old-btn" onclick="window.oldClick = 'ran'">old</button>
<button id="new-btn">new</button>
<div id="old-div" style="color: rgb(0, 128, 0)">inline style attribute</div>
<div id="new-div" class="green">class from a stylesheet</div>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
app.js 给 #new-btn 挂 click 监听器,app.css 定义 .green。页面加载后点两个按钮,再用两种方式给新建的元素设样式(el.style.color = .. 和 setAttribute('style', ..)),读回计算出的颜色和 securitypolicyviolation 事件。配置照「放宽内联」的习惯写:
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost"
}
CSP header: Some("default-src 'self'; connect-src 'self' ipc: http://ipc.localhost; script-src 'self' 'unsafe-inline' 'sha256-zFMSAynROrBaz5R/+AX7qFwU4iqM7pJkzvM9/yBt8sI=' 'sha256-0btd82xXAXmRWs1vrp79fmaC5G73K6nsrG5aG7bfGLU=' 'sha256-kTmVqQPNwuAopeFi3tCBKxCMI94mVDeB2siT8L8pY18='; style-src 'self' 'unsafe-inline' 'nonce-6678447660101119677'")
REPORT {
"onclickAttribute": "blocked",
"addEventListener": "ran",
"styleAttribute": "rgb(0, 0, 0)",
"styleClass": "rgb(0, 128, 0)",
"cssomStyle": "rgb(0, 0, 255)",
"setAttributeStyle": "rgb(0, 0, 0)",
"violations": [
"style-src-attr inline",
"script-src-attr inline",
"style-src-attr inline"
]
}
onclick属性没执行(script-src-attr),style="color: ..."属性没生效,字是默认的黑色(style-src-attr)。配置里两条指令都写着'unsafe-inline'。- 运行时
setAttribute('style', ..)同样被拦(第二条style-src-attr);el.style.color = ..走 CSSOM,不受影响,是蓝色。 - 响应头里,
script-src多了三个哈希:第一个是app.js的(openssl dgst -sha256 -binary dist/app.js | base64得到同一个值),后两个是页面里的两段内联<script>;style-src多了一个 nonce,给<style>标签。
出处
set_csp 对每个 HTML 响应调 replace_csp_nonce,只要有 nonce 或哈希,就追加到对应指令后面:
if !(nonces.is_empty() && hashes.is_empty()) {
let nonce_sources = nonces
.into_iter()
.map(|n| format!("'nonce-{n}'"))
.collect::<Vec<String>>();
let sources = csp.entry(directive.into()).or_default();
let self_source = "'self'".to_string();
if !sources.contains(&self_source) {
sources.push(self_source);
}
sources.extend(nonce_sources);
sources.extend(hashes);
}
CSP 规范规定:一条指令的源列表里只要出现了哈希或 nonce,'unsafe-inline' 就被忽略。这条规则是为了让一份 CSP 能同时写给认哈希和不认哈希的浏览器,落到 tauri 这里就成了:script-src 只要有一个 .js 文件或一段内联脚本,就一定有哈希,'unsafe-inline' 就一定失效;style-src 只要页面里有一个 <style> 标签,就有 nonce,同样失效。内联事件处理属性和 style 属性没有哈希、也挂不了 nonce,于是全部被拦。页面里没有 <style> 的 HTML,style-src 不会被改写(tauri:// 协议与 CSP 里 about.html 就是这样),同一个应用里有的页面 style 属性生效、有的不生效,原因就在这里。
修法
一:不写内联属性。 这是同一次运行里的另一半:addEventListener 挂的监听器执行了("addEventListener": "ran"),类名的样式生效("styleClass": "rgb(0, 128, 0)"),el.style.xxx = .. 也照常("cssomStyle")。事件处理放进 .js 文件,样式放进类名,动态样式用 CSSOM 而不是 setAttribute('style', ..)。CSP 里的 'unsafe-inline' 可以删掉,它本来就不起作用。
二:'unsafe-hashes' 加属性内容的哈希。 属性数量少、改不了 HTML 时,CSP3 允许给内联属性单独放行:算出属性值的哈希,和 'unsafe-hashes' 一起写进指令:
$ printf "%s" "window.oldClick = 'ran'" | openssl dgst -sha256 -binary | base64
fjUAO8nhPufN76jmgNF43iBCsNvswBKVkWZtiIH6yBo=
$ printf "%s" "color: rgb(0, 128, 0)" | openssl dgst -sha256 -binary | base64
cgrWcJeXGgvsbzqeXr9ARPkciN38f5Rd6u0ahueqhkU=
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-hashes' 'sha256-fjUAO8nhPufN76jmgNF43iBCsNvswBKVkWZtiIH6yBo='; style-src 'self' 'unsafe-hashes' 'sha256-cgrWcJeXGgvsbzqeXr9ARPkciN38f5Rd6u0ahueqhkU='; connect-src 'self' ipc: http://ipc.localhost"
}
CSP header: Some("style-src 'self' 'unsafe-hashes' 'sha256-cgrWcJeXGgvsbzqeXr9ARPkciN38f5Rd6u0ahueqhkU=' 'nonce-16006118651244409719'; connect-src 'self' ipc: http://ipc.localhost; default-src 'self'; script-src 'self' 'unsafe-hashes' 'sha256-fjUAO8nhPufN76jmgNF43iBCsNvswBKVkWZtiIH6yBo=' 'sha256-zFMSAynROrBaz5R/+AX7qFwU4iqM7pJkzvM9/yBt8sI=' 'sha256-0btd82xXAXmRWs1vrp79fmaC5G73K6nsrG5aG7bfGLU=' 'sha256-kTmVqQPNwuAopeFi3tCBKxCMI94mVDeB2siT8L8pY18='")
REPORT {
"onclickAttribute": "ran",
"addEventListener": "ran",
"styleAttribute": "rgb(0, 128, 0)",
"styleClass": "rgb(0, 128, 0)",
"cssomStyle": "rgb(0, 0, 255)",
"setAttributeStyle": "rgb(0, 0, 0)",
"violations": [
"style-src-attr inline"
]
}
tauri 的追加和手写的哈希共存:onclick 和那个 style 属性都放行了。setAttribute('style', 'color: rgb(0, 0, 255)') 仍被拦,因为它的内容不同,哈希不在列表里。哈希是按字节算的,属性值改一个空格就要重算。
三:按指令关掉 tauri 的改写。 dangerousDisableAssetCspModification 写成指令列表,只对列出的指令不追加:
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost",
"dangerousDisableAssetCspModification": ["script-src", "style-src"]
}
CSP header: Some("script-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost; default-src 'self'; style-src 'self' 'unsafe-inline'")
REPORT {
"onclickAttribute": "ran",
"addEventListener": "ran",
"styleAttribute": "rgb(0, 128, 0)",
"styleClass": "rgb(0, 128, 0)",
"cssomStyle": "rgb(0, 0, 255)",
"setAttributeStyle": "rgb(0, 0, 255)",
"violations": []
}
全部放行,响应头就是配置的原文。代价是 script-src 回到了「所有内联脚本都能执行」:任何能往页面里注入一段 <script> 的漏洞都能直接执行代码,哈希提供的保护没有了。只列 style-src 是一个折中,脚本仍按哈希放行,只对样式放宽:
CSP header: Some("connect-src 'self' ipc: http://ipc.localhost; default-src 'self'; script-src 'self' 'unsafe-inline' 'sha256-zFMSAynROrBaz5R/+AX7qFwU4iqM7pJkzvM9/yBt8sI=' 'sha256-0btd82xXAXmRWs1vrp79fmaC5G73K6nsrG5aG7bfGLU=' 'sha256-kTmVqQPNwuAopeFi3tCBKxCMI94mVDeB2siT8L8pY18='; style-src 'self' 'unsafe-inline'")
REPORT {
"onclickAttribute": "blocked",
"addEventListener": "ran",
"styleAttribute": "rgb(0, 128, 0)",
"styleClass": "rgb(0, 128, 0)",
"cssomStyle": "rgb(0, 0, 255)",
"setAttributeStyle": "rgb(0, 0, 255)",
"violations": [
"script-src-attr inline"
]
}
style 属性都生效了,onclick 照样被拦。
怎么确认没中招
- 在页面最前面挂
securitypolicyviolation监听器,开发构建里把violatedDirective报给 Rust。script-src-attr和style-src-attr就是这一类。 - 搜前端构建产物(不是源码,框架可能生成内联属性)里的
onclick=、onload=、style=、javascript:。 - 看实际的响应头:页面里
(await fetch(location.href)).headers.get('content-security-policy'),或者像本页这样在 Rust 侧用on_web_resource_request打印。头里同一条指令既有'unsafe-inline'又有sha256-/nonce-,前者就是摆设。
小结
- tauri 给
script-src追加.js文件和内联脚本的哈希,给style-src追加<style>的 nonce;按 CSP 规则,同一条指令里的'unsafe-inline'随之失效。 - 失效后
onclick这类事件属性、style属性、setAttribute('style', ..)都被拦;addEventListener、类名、CSSOM(el.style.x = ..)不受影响。 - 改法按推荐顺序:不写内联属性;
'unsafe-hashes'加属性值的哈希;dangerousDisableAssetCspModification按指令关掉改写(关script-src就放弃了脚本哈希的保护)。