Skip to content

Commit

Permalink
feat(core): use a strict CSP on the isolation iframe (#9086)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Mar 5, 2024
1 parent bb23511 commit b5c7432
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changes/strict-csp-isolation-frame.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch:enhance
---

Use a strict content security policy on the isolation pattern iframe.
80 changes: 43 additions & 37 deletions core/tauri-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -38,11 +38,30 @@ pub struct ContextData {
pub capabilities: Option<Vec<PathBuf>>,
}

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<u8>, &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();
Expand All @@ -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);
Expand All @@ -101,16 +89,34 @@ fn map_isolation(
_options: &AssetOptions,
dir: PathBuf,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &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);

// 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()
}

Expand Down
15 changes: 12 additions & 3 deletions core/tauri-utils/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ fn with_head<F: FnOnce(&NodeRef)>(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();

Expand Down Expand Up @@ -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())
Expand Down
22 changes: 12 additions & 10 deletions core/tauri/scripts/isolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
29 changes: 16 additions & 13 deletions core/tauri/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R: Runtime>(
pub(crate) fn set_csp<R: Runtime>(
asset: &mut String,
assets: &Box<dyn Assets>,
assets: &impl std::borrow::Borrow<dyn Assets>,
asset_path: &AssetKey,
manager: &AppManager<R>,
csp: Csp,
) -> String {
) -> HashMap<String, CspDirectiveSources> {
let mut csp = csp.into();
let hash_strings =
assets
.borrow()
.csp_hashes(asset_path)
.fold(CspHashStrings::default(), |mut acc, hash| {
match hash {
Expand Down Expand Up @@ -98,15 +99,7 @@ fn set_csp<R: Runtime>(
);
}

#[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
Expand Down Expand Up @@ -396,7 +389,17 @@ impl<R: Runtime> AppManager<R> {
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()
Expand Down
7 changes: 6 additions & 1 deletion core/tauri/src/manager/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,12 @@ impl<R: Runtime> WebviewManager<R> {
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))
});
Expand Down
38 changes: 34 additions & 4 deletions core/tauri/src/protocol/isolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,55 @@

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<R: Runtime>(
manager: Arc<AppManager<R>>,
schema: &str,
assets: Arc<EmbeddedAssets>,
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<dyn Assets>;

pub fn get(assets: Arc<EmbeddedAssets>, 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,
};
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)
Expand Down
2 changes: 1 addition & 1 deletion tooling/cli/src/migrate/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ mod test {
},
"pattern": { "use": "brownfield" },
"security": {
"csp": "default-src: 'self' tauri:"
"csp": "default-src 'self' tauri:"
}
}
});
Expand Down

0 comments on commit b5c7432

Please sign in to comment.