From 6132f3f4feb64488ef618f690a4f06adce864d91 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 4 May 2021 23:31:05 -0300 Subject: [PATCH] feat(core): reintroduce CSP injection (#1704) --- .changes/csp.md | 7 ++ core/tauri-codegen/src/context.rs | 8 ++- core/tauri-codegen/src/embedded_assets.rs | 41 +++++++++-- core/tauri-utils/Cargo.toml | 2 + core/tauri-utils/src/config.rs | 24 ++++++- core/tauri-utils/src/html.rs | 71 +++++++++++++++++++ core/tauri-utils/src/lib.rs | 2 + examples/api/src-tauri/tauri.conf.json | 4 +- .../splashscreen/src-tauri/tauri.conf.json | 2 +- .../templates/src-tauri/tauri.conf.json | 2 +- 10 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 .changes/csp.md create mode 100644 core/tauri-utils/src/html.rs diff --git a/.changes/csp.md b/.changes/csp.md new file mode 100644 index 000000000..ec79a527d --- /dev/null +++ b/.changes/csp.md @@ -0,0 +1,7 @@ +--- +"tauri-codegen": patch +"tauri-utils": patch +"tauri": patch +--- + +Reintroduce `csp` injection, configured on `tauri.conf.json > tauri > security > csp`. diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index f6e37c743..f6970559d 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::embedded_assets::{EmbeddedAssets, EmbeddedAssetsError}; +use crate::embedded_assets::{AssetOptions, EmbeddedAssets, EmbeddedAssetsError}; use proc_macro2::TokenStream; use quote::quote; use std::path::PathBuf; @@ -37,7 +37,11 @@ pub fn context_codegen(data: ContextData) -> Result); +/// Options used to embed assets. +#[derive(Default)] +pub struct AssetOptions { + csp: Option, +} + +impl AssetOptions { + /// Creates the default asset options. + pub fn new() -> Self { + Self::default() + } + + /// Sets the content security policy to add to HTML files. + pub fn csp(mut self, csp: String) -> Self { + self.csp.replace(csp); + self + } +} + impl EmbeddedAssets { /// Compress a directory of assets, ready to be generated into a [`tauri_utils::assets::Assets`]. - pub fn new(path: &Path) -> Result { + pub fn new(path: &Path, options: AssetOptions) -> Result { WalkDir::new(&path) .follow_links(true) .into_iter() @@ -73,7 +93,7 @@ impl EmbeddedAssets { Ok(entry) if entry.file_type().is_dir() => None, // compress all files encountered - Ok(entry) => Some(Self::compress_file(path, entry.path())), + Ok(entry) => Some(Self::compress_file(path, entry.path(), &options)), // pass down error through filter to fail when encountering any error Err(error) => Some(Err(EmbeddedAssetsError::Walkdir { @@ -96,11 +116,22 @@ impl EmbeddedAssets { } /// Compress a file and spit out the information in a [`HashMap`] friendly form. - fn compress_file(prefix: &Path, path: &Path) -> Result { - let input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead { + fn compress_file( + prefix: &Path, + path: &Path, + options: &AssetOptions, + ) -> Result { + let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead { path: path.to_owned(), error, })?; + if let Some(csp) = &options.csp { + if path.extension() == Some(OsStr::new("html")) { + input = inject_csp(String::from_utf8_lossy(&input).into_owned(), csp) + .as_bytes() + .to_vec(); + } + } // we must canonicalize the base of our paths to allow long paths on windows let out_dir = std::env::var("OUT_DIR") diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index 741aad612..81a0d7619 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -16,6 +16,8 @@ thiserror = "1.0.24" phf = { version = "0.8", features = [ "macros" ] } zstd = "0.7" url = { version = "2.2", features = [ "serde" ] } +kuchiki = "0.8" +html5ever = "0.25" proc-macro2 = { version = "1.0", optional = true } quote = { version = "1.0", optional = true } diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 283b020c6..cee39f07b 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -164,6 +164,14 @@ impl Default for UpdaterConfig { } } +/// Security configuration. +#[derive(PartialEq, Deserialize, Debug, Clone, Default)] +#[serde(tag = "updater", rename_all = "camelCase")] +pub struct SecurityConfig { + /// Content security policy to inject to HTML files with the custom protocol. + pub csp: Option, +} + /// A CLI argument definition #[derive(PartialEq, Deserialize, Debug, Default, Clone)] #[serde(rename_all = "camelCase")] @@ -340,6 +348,9 @@ pub struct TauriConfig { /// The updater configuration. #[serde(default)] pub updater: UpdaterConfig, + /// The security configuration. + #[serde(default)] + pub security: SecurityConfig, } impl Default for TauriConfig { @@ -349,6 +360,7 @@ impl Default for TauriConfig { cli: None, bundle: BundleConfig::default(), updater: UpdaterConfig::default(), + security: SecurityConfig::default(), } } } @@ -756,14 +768,23 @@ mod build { } } + impl ToTokens for SecurityConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let csp = opt_str_lit(self.csp.as_ref()); + + literal_struct!(tokens, SecurityConfig, csp); + } + } + impl ToTokens for TauriConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let windows = vec_lit(&self.windows, identity); let cli = opt_lit(self.cli.as_ref()); let bundle = &self.bundle; let updater = &self.updater; + let security = &self.security; - literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater); + literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater, security); } } @@ -857,6 +878,7 @@ mod test { pubkey: None, endpoints: None, }, + security: SecurityConfig { csp: None }, }; // create a build config diff --git a/core/tauri-utils/src/html.rs b/core/tauri-utils/src/html.rs new file mode 100644 index 000000000..aee2163b2 --- /dev/null +++ b/core/tauri-utils/src/html.rs @@ -0,0 +1,71 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use html5ever::{ + interface::QualName, + namespace_url, ns, + tendril::{fmt::UTF8, NonAtomic, Tendril}, + LocalName, +}; +use kuchiki::{traits::*, Attribute, ExpandedName, NodeRef}; + +/// Injects a content security policy to the HTML. +pub fn inject_csp>>(html: H, csp: &str) -> String { + let document = kuchiki::parse_html().one(html); + if let Ok(ref head) = document.select_first("head") { + head.as_node().append(create_csp_meta_tag(csp)); + } else { + let head = NodeRef::new_element( + QualName::new(None, ns!(html), LocalName::from("head")), + None, + ); + head.append(create_csp_meta_tag(csp)); + document.prepend(head); + } + document.to_string() +} + +fn create_csp_meta_tag(csp: &str) -> NodeRef { + NodeRef::new_element( + QualName::new(None, ns!(html), LocalName::from("meta")), + vec![ + ( + ExpandedName::new(ns!(), LocalName::from("http-equiv")), + Attribute { + prefix: None, + value: "Content-Security-Policy".into(), + }, + ), + ( + ExpandedName::new(ns!(), LocalName::from("content")), + Attribute { + prefix: None, + value: csp.into(), + }, + ), + ], + ) +} + +#[cfg(test)] +mod tests { + #[test] + fn csp() { + let htmls = vec![ + "".to_string(), + "".to_string(), + ]; + for html in htmls { + let csp = "default-src 'self'; img-src https://*; child-src 'none';"; + let new = super::inject_csp(html, csp); + assert_eq!( + new, + format!( + r#""#, + csp + ) + ); + } + } +} diff --git a/core/tauri-utils/src/lib.rs b/core/tauri-utils/src/lib.rs index 6592c6741..6831085a0 100644 --- a/core/tauri-utils/src/lib.rs +++ b/core/tauri-utils/src/lib.rs @@ -9,6 +9,8 @@ pub mod assets; /// Tauri config definition. pub mod config; +/// Tauri HTML processing. +pub mod html; /// Platform helpers pub mod platform; /// Process helpers diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index b28d475b0..061ea912b 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -78,7 +78,7 @@ } ], "security": { - "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'" } } -} \ No newline at end of file +} diff --git a/examples/splashscreen/src-tauri/tauri.conf.json b/examples/splashscreen/src-tauri/tauri.conf.json index ba282a7ac..8d01c55de 100644 --- a/examples/splashscreen/src-tauri/tauri.conf.json +++ b/examples/splashscreen/src-tauri/tauri.conf.json @@ -42,7 +42,7 @@ } ], "security": { - "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'" }, "updater": { "active": false diff --git a/tooling/cli.rs/templates/src-tauri/tauri.conf.json b/tooling/cli.rs/templates/src-tauri/tauri.conf.json index 94f632b34..129db1757 100644 --- a/tooling/cli.rs/templates/src-tauri/tauri.conf.json +++ b/tooling/cli.rs/templates/src-tauri/tauri.conf.json @@ -61,7 +61,7 @@ } ], "security": { - "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" + "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'" } } }