mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-10-05 20:48:58 +03:00
feat(core): use a strict CSP on the isolation iframe (#9086)
This commit is contained in:
parent
bb23511ea8
commit
b5c7432769
5
.changes/strict-csp-isolation-frame.md
Normal file
5
.changes/strict-csp-isolation-frame.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:enhance
|
||||
---
|
||||
|
||||
Use a strict content security policy on the isolation pattern iframe.
|
@ -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<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();
|
||||
@ -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<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);
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
@ -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())
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
@ -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
|
||||
@ -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()
|
||||
|
@ -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))
|
||||
});
|
||||
|
@ -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<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,
|
||||
@ -23,6 +52,7 @@ pub fn get(assets: Arc<EmbeddedAssets>, 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)
|
||||
|
@ -649,7 +649,7 @@ mod test {
|
||||
},
|
||||
"pattern": { "use": "brownfield" },
|
||||
"security": {
|
||||
"csp": "default-src: 'self' tauri:"
|
||||
"csp": "default-src 'self' tauri:"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user