Rust in Everything
基于 tauri 2.12.5

'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 就放弃了脚本哈希的保护)。

💬 关联讨论

发起讨论
加载中...