From e9f79cc20f6fa7d8509b2be23cba8b20bb8d4a05 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 5 Mar 2024 12:54:38 -0300 Subject: [PATCH] feat(core): use a strict CSP on the isolation iframe --- .changes/strict-csp-isolation-frame.md | 5 ++ core/tauri-codegen/src/context.rs | 80 ++++++++++++++------------ core/tauri-utils/src/html.rs | 15 ++++- core/tauri/scripts/isolation.js | 22 +++---- core/tauri/src/manager/mod.rs | 29 +++++----- core/tauri/src/manager/webview.rs | 7 ++- core/tauri/src/protocol/isolation.rs | 38 ++++++++++-- tooling/cli/src/migrate/config.rs | 2 +- 8 files changed, 129 insertions(+), 69 deletions(-) create mode 100644 .changes/strict-csp-isolation-frame.md diff --git a/.changes/strict-csp-isolation-frame.md b/.changes/strict-csp-isolation-frame.md new file mode 100644 index 000000000000..c012713118ff --- /dev/null +++ b/.changes/strict-csp-isolation-frame.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +Use a strict content security policy on the isolation pattern iframe. diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index 024a2ed0e439..cb7281cfe479 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -18,7 +18,7 @@ use tauri_utils::acl::resolved::Resolved; use tauri_utils::assets::AssetKey; use tauri_utils::config::{CapabilityEntry, Config, FrontendDist, PatternKind}; use tauri_utils::html::{ - inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, + inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef, }; use tauri_utils::platform::Target; use tauri_utils::tokens::{map_lit, str_lit}; @@ -38,11 +38,30 @@ pub struct ContextData { pub capabilities: Option>, } +fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) { + if let Ok(inline_script_elements) = document.select("script:not(empty)") { + let mut scripts = Vec::new(); + for inline_script_el in inline_script_elements { + let script = inline_script_el.as_node().text_contents(); + let mut hasher = Sha256::new(); + hasher.update(&script); + let hash = hasher.finalize(); + scripts.push(format!( + "'sha256-{}'", + base64::engine::general_purpose::STANDARD.encode(hash) + )); + } + csp_hashes + .inline_scripts + .entry(key.clone().into()) + .or_default() + .append(&mut scripts); + } +} + fn map_core_assets( options: &AssetOptions, ) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> Result<(), EmbeddedAssetsError> { - #[cfg(feature = "isolation")] - let pattern = tauri_utils::html::PatternObject::from(&options.pattern); let csp = options.csp; let dangerous_disable_asset_csp_modification = options.dangerous_disable_asset_csp_modification.clone(); @@ -55,38 +74,7 @@ fn map_core_assets( inject_nonce_token(&document, &dangerous_disable_asset_csp_modification); if dangerous_disable_asset_csp_modification.can_modify("script-src") { - if let Ok(inline_script_elements) = document.select("script:not(empty)") { - let mut scripts = Vec::new(); - for inline_script_el in inline_script_elements { - let script = inline_script_el.as_node().text_contents(); - let mut hasher = Sha256::new(); - hasher.update(&script); - let hash = hasher.finalize(); - scripts.push(format!( - "'sha256-{}'", - base64::engine::general_purpose::STANDARD.encode(hash) - )); - } - csp_hashes - .inline_scripts - .entry(key.clone().into()) - .or_default() - .append(&mut scripts); - } - } - - #[cfg(feature = "isolation")] - if dangerous_disable_asset_csp_modification.can_modify("style-src") { - if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern { - // create the csp for the isolation iframe styling now, to make the runtime less complex - let mut hasher = Sha256::new(); - hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE); - let hash = hasher.finalize(); - csp_hashes.styles.push(format!( - "'sha256-{}'", - base64::engine::general_purpose::STANDARD.encode(hash) - )); - } + inject_script_hashes(&document, key, csp_hashes); } *input = serialize_html_node(&document); @@ -101,9 +89,18 @@ fn map_isolation( _options: &AssetOptions, dir: PathBuf, ) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> Result<(), EmbeddedAssetsError> { - move |_key, path, input, _csp_hashes| { + // create the csp for the isolation iframe styling now, to make the runtime less complex + let mut hasher = Sha256::new(); + hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE); + let hash = hasher.finalize(); + let iframe_style_csp_hash = format!( + "'sha256-{}'", + base64::engine::general_purpose::STANDARD.encode(hash) + ); + + move |key, path, input, csp_hashes| { if path.extension() == Some(OsStr::new("html")) { - let isolation_html = tauri_utils::html::parse(String::from_utf8_lossy(input).into_owned()); + let isolation_html = parse_html(String::from_utf8_lossy(input).into_owned()); // this is appended, so no need to reverse order it tauri_utils::html::inject_codegen_isolation_script(&isolation_html); @@ -111,6 +108,15 @@ fn map_isolation( // temporary workaround for windows not loading assets tauri_utils::html::inline_isolation(&isolation_html, &dir); + inject_nonce_token( + &isolation_html, + &tauri_utils::config::DisabledCspModificationKind::Flag(false), + ); + + inject_script_hashes(&isolation_html, key, csp_hashes); + + csp_hashes.styles.push(iframe_style_csp_hash.clone()); + *input = isolation_html.to_string().as_bytes().to_vec() } diff --git a/core/tauri-utils/src/html.rs b/core/tauri-utils/src/html.rs index 95d9d19595d1..97c83e2a35e3 100644 --- a/core/tauri-utils/src/html.rs +++ b/core/tauri-utils/src/html.rs @@ -131,8 +131,8 @@ fn with_head(document: &NodeRef, f: F) { } fn inject_nonce(document: &NodeRef, selector: &str, token: &str) { - if let Ok(scripts) = document.select(selector) { - for target in scripts { + if let Ok(elements) = document.select(selector) { + for target in elements { let node = target.as_node(); let element = node.as_element().unwrap(); @@ -234,7 +234,16 @@ impl Default for IsolationSide { #[cfg(feature = "isolation")] pub fn inject_codegen_isolation_script(document: &NodeRef) { with_head(document, |head| { - let script = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); + let script = NodeRef::new_element( + QualName::new(None, ns!(html), "script".into()), + vec![( + ExpandedName::new(ns!(), LocalName::from("nonce")), + Attribute { + prefix: None, + value: SCRIPT_NONCE_TOKEN.into(), + }, + )], + ); script.append(NodeRef::new_text( IsolationJavascriptCodegen {} .render_default(&Default::default()) diff --git a/core/tauri/scripts/isolation.js b/core/tauri/scripts/isolation.js index 9e8b712952ec..19a34c9baefa 100644 --- a/core/tauri/scripts/isolation.js +++ b/core/tauri/scripts/isolation.js @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -window.addEventListener('DOMContentLoaded', () => { - let style = document.createElement('style') - style.textContent = __TEMPLATE_style__ - document.head.append(style) +if (location.href !== __TEMPLATE_isolation_src__) { + window.addEventListener('DOMContentLoaded', () => { + let style = document.createElement('style') + style.textContent = __TEMPLATE_style__ + document.head.append(style) - let iframe = document.createElement('iframe') - iframe.id = '__tauri_isolation__' - iframe.sandbox.add('allow-scripts') - iframe.src = __TEMPLATE_isolation_src__ - document.body.append(iframe) -}) + let iframe = document.createElement('iframe') + iframe.id = '__tauri_isolation__' + iframe.sandbox.add('allow-scripts') + iframe.src = __TEMPLATE_isolation_src__ + document.body.append(iframe) + }) +} diff --git a/core/tauri/src/manager/mod.rs b/core/tauri/src/manager/mod.rs index 26e8a639b381..a1b49d09eb81 100644 --- a/core/tauri/src/manager/mod.rs +++ b/core/tauri/src/manager/mod.rs @@ -46,16 +46,17 @@ struct CspHashStrings { /// Sets the CSP value to the asset HTML if needed (on Linux). /// Returns the CSP string for access on the response header (on Windows and macOS). #[allow(clippy::borrowed_box)] -fn set_csp( +pub(crate) fn set_csp( asset: &mut String, - assets: &Box, + assets: &impl std::borrow::Borrow, asset_path: &AssetKey, manager: &AppManager, csp: Csp, -) -> String { +) -> HashMap { let mut csp = csp.into(); let hash_strings = assets + .borrow() .csp_hashes(asset_path) .fold(CspHashStrings::default(), |mut acc, hash| { match hash { @@ -98,15 +99,7 @@ fn set_csp( ); } - #[cfg(feature = "isolation")] - if let Pattern::Isolation { schema, .. } = &*manager.pattern { - let default_src = csp - .entry("default-src".into()) - .or_insert_with(Default::default); - default_src.push(crate::pattern::format_real_schema(schema)); - } - - Csp::DirectiveMap(csp).to_string() + csp } // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297 @@ -396,7 +389,17 @@ impl AppManager { let final_data = if is_html { let mut asset = String::from_utf8_lossy(&asset).into_owned(); if let Some(csp) = self.csp() { - csp_header.replace(set_csp(&mut asset, &self.assets, &asset_path, self, csp)); + #[allow(unused_mut)] + let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); + #[cfg(feature = "isolation")] + if let Pattern::Isolation { schema, .. } = &*self.pattern { + let default_src = csp_map + .entry("default-src".into()) + .or_insert_with(Default::default); + default_src.push(crate::pattern::format_real_schema(schema)); + } + + csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); } asset.as_bytes().to_vec() diff --git a/core/tauri/src/manager/webview.rs b/core/tauri/src/manager/webview.rs index cabee0706641..6547d6ffd585 100644 --- a/core/tauri/src/manager/webview.rs +++ b/core/tauri/src/manager/webview.rs @@ -313,7 +313,12 @@ impl WebviewManager { crypto_keys, } = &*app_manager.pattern { - let protocol = crate::protocol::isolation::get(assets.clone(), *crypto_keys.aes_gcm().raw()); + let protocol = crate::protocol::isolation::get( + manager.manager_owned(), + schema, + assets.clone(), + *crypto_keys.aes_gcm().raw(), + ); pending.register_uri_scheme_protocol(schema, move |request, responder| { protocol(request, UriSchemeResponder(responder)) }); diff --git a/core/tauri/src/protocol/isolation.rs b/core/tauri/src/protocol/isolation.rs index f98b8682bf3a..918e26d57682 100644 --- a/core/tauri/src/protocol/isolation.rs +++ b/core/tauri/src/protocol/isolation.rs @@ -4,18 +4,47 @@ use http::header::CONTENT_TYPE; use serialize_to_javascript::Template; -use tauri_utils::assets::{Assets, EmbeddedAssets}; +use tauri_utils::{ + assets::{Assets, EmbeddedAssets}, + config::Csp, +}; use std::sync::Arc; -use crate::{manager::webview::PROCESS_IPC_MESSAGE_FN, webview::UriSchemeProtocolHandler}; +use crate::{ + manager::{set_csp, webview::PROCESS_IPC_MESSAGE_FN, AppManager}, + webview::UriSchemeProtocolHandler, + Runtime, +}; + +pub fn get( + manager: Arc>, + schema: &str, + assets: Arc, + aes_gcm_key: [u8; 32], +) -> UriSchemeProtocolHandler { + let frame_src = if cfg!(any(windows, target_os = "android")) { + format!("http://{schema}.localhost") + } else { + format!("{schema}:") + }; + + let assets = assets as Arc; -pub fn get(assets: Arc, aes_gcm_key: [u8; 32]) -> UriSchemeProtocolHandler { Box::new(move |request, responder| { let response = match request_to_path(&request).as_str() { "index.html" => match assets.get(&"index.html".into()) { Some(asset) => { - let asset = String::from_utf8_lossy(asset.as_ref()); + let mut asset = String::from_utf8_lossy(asset.as_ref()).into_owned(); + let csp_map = set_csp( + &mut asset, + &assets, + &"index.html".into(), + &manager, + Csp::Policy(format!("default-src 'none'; frame-src {}", frame_src)), + ); + let csp = Csp::DirectiveMap(csp_map).to_string(); + let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { runtime_aes_gcm_key: &aes_gcm_key, process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, @@ -23,6 +52,7 @@ pub fn get(assets: Arc, aes_gcm_key: [u8; 32]) -> UriSchemeProto match template.render(asset.as_ref(), &Default::default()) { Ok(asset) => http::Response::builder() .header(CONTENT_TYPE, mime::TEXT_HTML.as_ref()) + .header("Content-Security-Policy", csp) .body(asset.into_string().as_bytes().to_vec()), Err(_) => http::Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) diff --git a/tooling/cli/src/migrate/config.rs b/tooling/cli/src/migrate/config.rs index 2229f61bb678..91d356e408d2 100644 --- a/tooling/cli/src/migrate/config.rs +++ b/tooling/cli/src/migrate/config.rs @@ -649,7 +649,7 @@ mod test { }, "pattern": { "use": "brownfield" }, "security": { - "csp": "default-src: 'self' tauri:" + "csp": "default-src 'self' tauri:" } } });