Skip to content

Commit 943238f

Browse files
committed
fix(review): 快照脚本/事件属性/javascript URL 全剥(离线零执行面)
1 parent a855cbc commit 943238f

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

‎app/src-tauri/src/web_snapshot.rs‎

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,15 @@ pub fn inline_html(base: &str, html: &str, resolver: Resolver) -> String {
102102
let data = data_or_keep("img", resolve_url(base, &src), &mut *resolver);
103103
if let Some(d) = data {
104104
let replaced = replace_attr(tag, "src", &d);
105-
out.push_str(&replaced);
105+
// 内联化后仍须过净化(原标签其余 on*/危险属性一并剥除)
106+
out.push_str(&scrub_tag(&replaced));
106107
continue;
107108
}
108109
}
109-
out.push_str(tag);
110-
} else if lower.starts_with("<script") && tag.contains("src=") {
111-
// 外链脚本剔除(防快照执行第三方 JS——只存档不执行),连同闭合标签
112-
out.push_str("<!-- entropy-snapshot: external script removed -->");
110+
out.push_str(&scrub_tag(tag));
111+
} else if lower.starts_with("<script") {
112+
// 全部 script 剔除(外链+行内——快照离线打开语境不得执行任何原文脚本)
113+
out.push_str("<!-- entropy-snapshot: script removed -->");
113114
let lower_rest = rest.to_ascii_lowercase();
114115
if let Some(close_idx) = lower_rest.find("</script") {
115116
if let Some(gt) = rest[close_idx..].find('>') {
@@ -119,7 +120,8 @@ pub fn inline_html(base: &str, html: &str, resolver: Resolver) -> String {
119120
}
120121
// 无闭合标签的畸形脚本:继续正常扫描
121122
} else {
122-
out.push_str(tag);
123+
// 通用标签净化:on* 事件属性与 javascript:/data:text/html URL 剥除
124+
out.push_str(&scrub_tag(tag));
123125
}
124126
}
125127
out.push_str(rest);
@@ -172,6 +174,55 @@ fn escape_attr(s: &str) -> String {
172174
s.replace('"', "&quot;")
173175
}
174176

177+
/// 标签净化(纯函数):剥除 on* 事件属性与 javascript:/data:text/html 危险
178+
/// URL——快照是离线下发的 HTML 文档,任何可执行面归零(安全边界)。
179+
fn scrub_tag(tag: &str) -> String {
180+
// '>' 边界单独保留(不参与属性 token 判定——防整段误吞闭合符)
181+
let (body, closer) = match tag.rfind('>') {
182+
Some(i) => (&tag[..i], &tag[i..]),
183+
None => (tag, ""),
184+
};
185+
let mut out = String::new();
186+
let mut rest = body;
187+
let mut first = true;
188+
while let Some(ws) = rest.find(|c: char| c.is_whitespace()) {
189+
let token = &rest[..ws];
190+
rest = rest[ws..].trim_start();
191+
if first {
192+
out.push_str(token);
193+
first = false;
194+
continue;
195+
}
196+
if !token.is_empty() && !dangerous_attr(token) {
197+
out.push(' ');
198+
out.push_str(token);
199+
}
200+
}
201+
if first {
202+
out.push_str(rest);
203+
} else if !rest.is_empty() && !dangerous_attr(rest) {
204+
out.push(' ');
205+
out.push_str(rest);
206+
}
207+
out.push_str(closer);
208+
out
209+
}
210+
211+
fn dangerous_attr(token: &str) -> bool {
212+
let name: String = token.chars().take_while(|c| *c != '=').collect();
213+
let lower_name = name.to_ascii_lowercase();
214+
if lower_name.starts_with("on") {
215+
return true; // 事件属性(onclick/onload/onerror…)
216+
}
217+
let lower_token = token.to_ascii_lowercase();
218+
if lower_token.contains("javascript:") || lower_token.contains("data:text/html") {
219+
return true;
220+
}
221+
// <a>/<img> 之外标签的 data:image 保留(仅图片类 data URI 安全);href 上
222+
// 的 data:text/html 已被上一行拦截
223+
false
224+
}
225+
175226
#[cfg(test)]
176227
#[path = "web_snapshot_tests.rs"]
177228
mod tests;

‎app/src-tauri/src/web_snapshot_tests.rs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,23 @@ fn inline_styles_imgs_and_strip_external_scripts() {
2626
assert!(out.contains("data:text/css;base64,aGFzaA=="), "{}", out);
2727
assert!(out.contains("data:image/png;base64,aWNvbg=="), "{}", out);
2828
assert!(!out.contains("evil.example"), "外链脚本剔除");
29-
assert!(out.contains("var ok=1"), "行内脚本保持原文(快照只存不开)");
29+
assert!(!out.contains("var ok=1"), "行内脚本同样剔除(离线打开零执行面)");
3030
assert!(out.contains("<p>正文</p>"));
3131
}
3232

33+
#[test]
34+
fn scrub_removes_event_attrs_and_javascript_urls() {
35+
let html = r#"<a href="javascript:alert(1)" onclick="x()" data-x="1">点我</a><img src="pic/a.png" onerror="x()"><iframe src="javascript:void(0)"></iframe>"#;
36+
let mut resolver = |url: &str| {
37+
if url.ends_with("pic/a.png") { Some("aQ==".to_string()) } else { None }
38+
};
39+
let out = inline_html("https://a.com/x/", html, &mut resolver);
40+
assert!(!out.contains("javascript:"), "{}", out);
41+
assert!(!out.contains("onclick"), "{}", out);
42+
assert!(!out.contains("onerror"), "{}", out);
43+
assert!(out.contains(">点我</a>") || out.contains("点我"), "{}", out);
44+
}
45+
3346
#[test]
3447
fn unresolvable_assets_keep_original_reference() {
3548
let html = r#"<img src="missing.png"><link rel="stylesheet" href="/gone.css">"#;

0 commit comments

Comments
 (0)