Răsfoiți Sursa

feat(core): reintroduce CSP injection (#1704)

Lucas Fernandes Nogueira 4 ani în urmă
părinte
comite
6132f3f4fe

+ 7 - 0
.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`.

+ 6 - 2
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<TokenStream, EmbeddedAssetsE
 
   // generate the assets inside the dist dir into a perfect hash function
   let assets = if let Some(assets_path) = assets_path {
-    EmbeddedAssets::new(&assets_path)?
+    let mut options = AssetOptions::new();
+    if let Some(csp) = &config.tauri.security.csp {
+      options = options.csp(csp.clone());
+    }
+    EmbeddedAssets::new(&assets_path, options)?
   } else {
     Default::default()
   };

+ 36 - 5
core/tauri-codegen/src/embedded_assets.rs

@@ -6,10 +6,11 @@ use proc_macro2::TokenStream;
 use quote::{quote, ToTokens, TokenStreamExt};
 use std::{
   collections::HashMap,
+  ffi::OsStr,
   fs::File,
   path::{Path, PathBuf},
 };
-use tauri_utils::assets::AssetKey;
+use tauri_utils::{assets::AssetKey, html::inject_csp};
 use thiserror::Error;
 use walkdir::WalkDir;
 
@@ -62,9 +63,28 @@ pub enum EmbeddedAssetsError {
 #[derive(Default)]
 pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
 
+/// Options used to embed assets.
+#[derive(Default)]
+pub struct AssetOptions {
+  csp: Option<String>,
+}
+
+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<Self, EmbeddedAssetsError> {
+  pub fn new(path: &Path, options: AssetOptions) -> Result<Self, EmbeddedAssetsError> {
     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<Asset, EmbeddedAssetsError> {
-    let input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
+  fn compress_file(
+    prefix: &Path,
+    path: &Path,
+    options: &AssetOptions,
+  ) -> Result<Asset, EmbeddedAssetsError> {
+    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")

+ 2 - 0
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 }
 

+ 23 - 1
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<String>,
+}
+
 /// 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

+ 71 - 0
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<H: Into<Tendril<UTF8, NonAtomic>>>(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![
+      "<html><head></head></html>".to_string(),
+      "<html></html>".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#"<html><head><meta content="{}" http-equiv="Content-Security-Policy"></head><body></body></html>"#,
+          csp
+        )
+      );
+    }
+  }
+}

+ 2 - 0
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

+ 2 - 2
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'"
     }
   }
-}
+}

+ 1 - 1
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

+ 1 - 1
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'"
     }
   }
 }