Skip to content

Commit b5c7432

Browse files
authored
feat(core): use a strict CSP on the isolation iframe (#9086)
1 parent bb23511 commit b5c7432

File tree

8 files changed

+129
-69
lines changed

8 files changed

+129
-69
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch:enhance
3+
---
4+
5+
Use a strict content security policy on the isolation pattern iframe.

core/tauri-codegen/src/context.rs

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use tauri_utils::acl::resolved::Resolved;
1818
use tauri_utils::assets::AssetKey;
1919
use tauri_utils::config::{CapabilityEntry, Config, FrontendDist, PatternKind};
2020
use tauri_utils::html::{
21-
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
21+
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef,
2222
};
2323
use tauri_utils::platform::Target;
2424
use tauri_utils::tokens::{map_lit, str_lit};
@@ -38,11 +38,30 @@ pub struct ContextData {
3838
pub capabilities: Option<Vec<PathBuf>>,
3939
}
4040

41+
fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
42+
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
43+
let mut scripts = Vec::new();
44+
for inline_script_el in inline_script_elements {
45+
let script = inline_script_el.as_node().text_contents();
46+
let mut hasher = Sha256::new();
47+
hasher.update(&script);
48+
let hash = hasher.finalize();
49+
scripts.push(format!(
50+
"'sha256-{}'",
51+
base64::engine::general_purpose::STANDARD.encode(hash)
52+
));
53+
}
54+
csp_hashes
55+
.inline_scripts
56+
.entry(key.clone().into())
57+
.or_default()
58+
.append(&mut scripts);
59+
}
60+
}
61+
4162
fn map_core_assets(
4263
options: &AssetOptions,
4364
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
44-
#[cfg(feature = "isolation")]
45-
let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
4665
let csp = options.csp;
4766
let dangerous_disable_asset_csp_modification =
4867
options.dangerous_disable_asset_csp_modification.clone();
@@ -55,38 +74,7 @@ fn map_core_assets(
5574
inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);
5675

5776
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
58-
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
59-
let mut scripts = Vec::new();
60-
for inline_script_el in inline_script_elements {
61-
let script = inline_script_el.as_node().text_contents();
62-
let mut hasher = Sha256::new();
63-
hasher.update(&script);
64-
let hash = hasher.finalize();
65-
scripts.push(format!(
66-
"'sha256-{}'",
67-
base64::engine::general_purpose::STANDARD.encode(hash)
68-
));
69-
}
70-
csp_hashes
71-
.inline_scripts
72-
.entry(key.clone().into())
73-
.or_default()
74-
.append(&mut scripts);
75-
}
76-
}
77-
78-
#[cfg(feature = "isolation")]
79-
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
80-
if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
81-
// create the csp for the isolation iframe styling now, to make the runtime less complex
82-
let mut hasher = Sha256::new();
83-
hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
84-
let hash = hasher.finalize();
85-
csp_hashes.styles.push(format!(
86-
"'sha256-{}'",
87-
base64::engine::general_purpose::STANDARD.encode(hash)
88-
));
89-
}
77+
inject_script_hashes(&document, key, csp_hashes);
9078
}
9179

9280
*input = serialize_html_node(&document);
@@ -101,16 +89,34 @@ fn map_isolation(
10189
_options: &AssetOptions,
10290
dir: PathBuf,
10391
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
104-
move |_key, path, input, _csp_hashes| {
92+
// create the csp for the isolation iframe styling now, to make the runtime less complex
93+
let mut hasher = Sha256::new();
94+
hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
95+
let hash = hasher.finalize();
96+
let iframe_style_csp_hash = format!(
97+
"'sha256-{}'",
98+
base64::engine::general_purpose::STANDARD.encode(hash)
99+
);
100+
101+
move |key, path, input, csp_hashes| {
105102
if path.extension() == Some(OsStr::new("html")) {
106-
let isolation_html = tauri_utils::html::parse(String::from_utf8_lossy(input).into_owned());
103+
let isolation_html = parse_html(String::from_utf8_lossy(input).into_owned());
107104

108105
// this is appended, so no need to reverse order it
109106
tauri_utils::html::inject_codegen_isolation_script(&isolation_html);
110107

111108
// temporary workaround for windows not loading assets
112109
tauri_utils::html::inline_isolation(&isolation_html, &dir);
113110

111+
inject_nonce_token(
112+
&isolation_html,
113+
&tauri_utils::config::DisabledCspModificationKind::Flag(false),
114+
);
115+
116+
inject_script_hashes(&isolation_html, key, csp_hashes);
117+
118+
csp_hashes.styles.push(iframe_style_csp_hash.clone());
119+
114120
*input = isolation_html.to_string().as_bytes().to_vec()
115121
}
116122

core/tauri-utils/src/html.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ fn with_head<F: FnOnce(&NodeRef)>(document: &NodeRef, f: F) {
131131
}
132132

133133
fn inject_nonce(document: &NodeRef, selector: &str, token: &str) {
134-
if let Ok(scripts) = document.select(selector) {
135-
for target in scripts {
134+
if let Ok(elements) = document.select(selector) {
135+
for target in elements {
136136
let node = target.as_node();
137137
let element = node.as_element().unwrap();
138138

@@ -234,7 +234,16 @@ impl Default for IsolationSide {
234234
#[cfg(feature = "isolation")]
235235
pub fn inject_codegen_isolation_script(document: &NodeRef) {
236236
with_head(document, |head| {
237-
let script = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
237+
let script = NodeRef::new_element(
238+
QualName::new(None, ns!(html), "script".into()),
239+
vec![(
240+
ExpandedName::new(ns!(), LocalName::from("nonce")),
241+
Attribute {
242+
prefix: None,
243+
value: SCRIPT_NONCE_TOKEN.into(),
244+
},
245+
)],
246+
);
238247
script.append(NodeRef::new_text(
239248
IsolationJavascriptCodegen {}
240249
.render_default(&Default::default())

core/tauri/scripts/isolation.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
window.addEventListener('DOMContentLoaded', () => {
6-
let style = document.createElement('style')
7-
style.textContent = __TEMPLATE_style__
8-
document.head.append(style)
5+
if (location.href !== __TEMPLATE_isolation_src__) {
6+
window.addEventListener('DOMContentLoaded', () => {
7+
let style = document.createElement('style')
8+
style.textContent = __TEMPLATE_style__
9+
document.head.append(style)
910

10-
let iframe = document.createElement('iframe')
11-
iframe.id = '__tauri_isolation__'
12-
iframe.sandbox.add('allow-scripts')
13-
iframe.src = __TEMPLATE_isolation_src__
14-
document.body.append(iframe)
15-
})
11+
let iframe = document.createElement('iframe')
12+
iframe.id = '__tauri_isolation__'
13+
iframe.sandbox.add('allow-scripts')
14+
iframe.src = __TEMPLATE_isolation_src__
15+
document.body.append(iframe)
16+
})
17+
}

core/tauri/src/manager/mod.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,17 @@ struct CspHashStrings {
4646
/// Sets the CSP value to the asset HTML if needed (on Linux).
4747
/// Returns the CSP string for access on the response header (on Windows and macOS).
4848
#[allow(clippy::borrowed_box)]
49-
fn set_csp<R: Runtime>(
49+
pub(crate) fn set_csp<R: Runtime>(
5050
asset: &mut String,
51-
assets: &Box<dyn Assets>,
51+
assets: &impl std::borrow::Borrow<dyn Assets>,
5252
asset_path: &AssetKey,
5353
manager: &AppManager<R>,
5454
csp: Csp,
55-
) -> String {
55+
) -> HashMap<String, CspDirectiveSources> {
5656
let mut csp = csp.into();
5757
let hash_strings =
5858
assets
59+
.borrow()
5960
.csp_hashes(asset_path)
6061
.fold(CspHashStrings::default(), |mut acc, hash| {
6162
match hash {
@@ -98,15 +99,7 @@ fn set_csp<R: Runtime>(
9899
);
99100
}
100101

101-
#[cfg(feature = "isolation")]
102-
if let Pattern::Isolation { schema, .. } = &*manager.pattern {
103-
let default_src = csp
104-
.entry("default-src".into())
105-
.or_insert_with(Default::default);
106-
default_src.push(crate::pattern::format_real_schema(schema));
107-
}
108-
109-
Csp::DirectiveMap(csp).to_string()
102+
csp
110103
}
111104

112105
// inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
@@ -396,7 +389,17 @@ impl<R: Runtime> AppManager<R> {
396389
let final_data = if is_html {
397390
let mut asset = String::from_utf8_lossy(&asset).into_owned();
398391
if let Some(csp) = self.csp() {
399-
csp_header.replace(set_csp(&mut asset, &self.assets, &asset_path, self, csp));
392+
#[allow(unused_mut)]
393+
let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp);
394+
#[cfg(feature = "isolation")]
395+
if let Pattern::Isolation { schema, .. } = &*self.pattern {
396+
let default_src = csp_map
397+
.entry("default-src".into())
398+
.or_insert_with(Default::default);
399+
default_src.push(crate::pattern::format_real_schema(schema));
400+
}
401+
402+
csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
400403
}
401404

402405
asset.as_bytes().to_vec()

core/tauri/src/manager/webview.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,12 @@ impl<R: Runtime> WebviewManager<R> {
313313
crypto_keys,
314314
} = &*app_manager.pattern
315315
{
316-
let protocol = crate::protocol::isolation::get(assets.clone(), *crypto_keys.aes_gcm().raw());
316+
let protocol = crate::protocol::isolation::get(
317+
manager.manager_owned(),
318+
schema,
319+
assets.clone(),
320+
*crypto_keys.aes_gcm().raw(),
321+
);
317322
pending.register_uri_scheme_protocol(schema, move |request, responder| {
318323
protocol(request, UriSchemeResponder(responder))
319324
});

core/tauri/src/protocol/isolation.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,55 @@
44

55
use http::header::CONTENT_TYPE;
66
use serialize_to_javascript::Template;
7-
use tauri_utils::assets::{Assets, EmbeddedAssets};
7+
use tauri_utils::{
8+
assets::{Assets, EmbeddedAssets},
9+
config::Csp,
10+
};
811

912
use std::sync::Arc;
1013

11-
use crate::{manager::webview::PROCESS_IPC_MESSAGE_FN, webview::UriSchemeProtocolHandler};
14+
use crate::{
15+
manager::{set_csp, webview::PROCESS_IPC_MESSAGE_FN, AppManager},
16+
webview::UriSchemeProtocolHandler,
17+
Runtime,
18+
};
19+
20+
pub fn get<R: Runtime>(
21+
manager: Arc<AppManager<R>>,
22+
schema: &str,
23+
assets: Arc<EmbeddedAssets>,
24+
aes_gcm_key: [u8; 32],
25+
) -> UriSchemeProtocolHandler {
26+
let frame_src = if cfg!(any(windows, target_os = "android")) {
27+
format!("http://{schema}.localhost")
28+
} else {
29+
format!("{schema}:")
30+
};
31+
32+
let assets = assets as Arc<dyn Assets>;
1233

13-
pub fn get(assets: Arc<EmbeddedAssets>, aes_gcm_key: [u8; 32]) -> UriSchemeProtocolHandler {
1434
Box::new(move |request, responder| {
1535
let response = match request_to_path(&request).as_str() {
1636
"index.html" => match assets.get(&"index.html".into()) {
1737
Some(asset) => {
18-
let asset = String::from_utf8_lossy(asset.as_ref());
38+
let mut asset = String::from_utf8_lossy(asset.as_ref()).into_owned();
39+
let csp_map = set_csp(
40+
&mut asset,
41+
&assets,
42+
&"index.html".into(),
43+
&manager,
44+
Csp::Policy(format!("default-src 'none'; frame-src {}", frame_src)),
45+
);
46+
let csp = Csp::DirectiveMap(csp_map).to_string();
47+
1948
let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
2049
runtime_aes_gcm_key: &aes_gcm_key,
2150
process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN,
2251
};
2352
match template.render(asset.as_ref(), &Default::default()) {
2453
Ok(asset) => http::Response::builder()
2554
.header(CONTENT_TYPE, mime::TEXT_HTML.as_ref())
55+
.header("Content-Security-Policy", csp)
2656
.body(asset.into_string().as_bytes().to_vec()),
2757
Err(_) => http::Response::builder()
2858
.status(http::StatusCode::INTERNAL_SERVER_ERROR)

tooling/cli/src/migrate/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ mod test {
649649
},
650650
"pattern": { "use": "brownfield" },
651651
"security": {
652-
"csp": "default-src: 'self' tauri:"
652+
"csp": "default-src 'self' tauri:"
653653
}
654654
}
655655
});

0 commit comments

Comments
 (0)