Browse Source

feat(core): inject `CSP` on data URLs [TRI-049] (#16)

Lucas Nogueira 3 years ago
parent
commit
8259cd64c2

+ 5 - 0
.changes/data-url-csp.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Inject configured `CSP` on `data:` URLs.

+ 0 - 1
core/tauri-codegen/Cargo.toml

@@ -24,7 +24,6 @@ tauri-utils = { version = "1.0.0-beta.3", path = "../tauri-utils", features = [
 thiserror = "1"
 walkdir = "2"
 zstd = { version = "0.9", optional = true }
-kuchiki = "0.8"
 regex = "1"
 
 [features]

+ 2 - 3
core/tauri-codegen/src/embedded_assets.rs

@@ -2,7 +2,6 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use kuchiki::traits::*;
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens, TokenStreamExt};
 use regex::RegexSet;
@@ -15,7 +14,7 @@ use std::{
 };
 use tauri_utils::{
   assets::AssetKey,
-  html::{inject_invoke_key_token, inject_nonce_token},
+  html::{inject_invoke_key_token, inject_nonce_token, parse as parse_html},
 };
 use thiserror::Error;
 use walkdir::{DirEntry, WalkDir};
@@ -253,7 +252,7 @@ impl EmbeddedAssets {
       })?;
 
     if path.extension() == Some(OsStr::new("html")) {
-      let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&input).into_owned());
+      let mut document = parse_html(String::from_utf8_lossy(&input).into_owned());
       if options.csp {
         #[cfg(target_os = "linux")]
         ::tauri_utils::html::inject_csp_token(&mut document);

+ 15 - 5
core/tauri-utils/src/html.rs

@@ -4,7 +4,7 @@
 
 //! The module to process HTML in Tauri.
 
-use html5ever::{interface::QualName, namespace_url, ns, LocalName};
+use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName};
 use kuchiki::{Attribute, ExpandedName, NodeRef};
 
 /// The token used on the CSP tag content.
@@ -16,6 +16,11 @@ pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
 /// The token used for the invoke key.
 pub const INVOKE_KEY_TOKEN: &str = "__TAURI__INVOKE_KEY_TOKEN__";
 
+/// Parses the given HTML string.
+pub fn parse(html: String) -> NodeRef {
+  kuchiki::parse_html().one(html)
+}
+
 fn inject_nonce(document: &mut NodeRef, selector: &str, token: &str) {
   if let Ok(scripts) = document.select(selector) {
     for target in scripts {
@@ -108,20 +113,25 @@ pub fn inject_invoke_key_token(document: &mut NodeRef) {
   }
 }
 
-/// Injects a content security policy token to the HTML.
-pub fn inject_csp_token(document: &mut NodeRef) {
+/// Injects a content security policy to the HTML.
+pub fn inject_csp(document: &mut NodeRef, csp: &str) {
   if let Ok(ref head) = document.select_first("head") {
-    head.as_node().append(create_csp_meta_tag(CSP_TOKEN));
+    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_TOKEN));
+    head.append(create_csp_meta_tag(csp));
     document.prepend(head);
   }
 }
 
+/// Injects a content security policy token to the HTML.
+pub fn inject_csp_token(document: &mut NodeRef) {
+  inject_csp(document, CSP_TOKEN)
+}
+
 fn create_csp_meta_tag(csp: &str) -> NodeRef {
   NodeRef::new_element(
     QualName::new(None, ns!(html), LocalName::from("meta")),

+ 1 - 0
core/tauri/Cargo.toml

@@ -76,6 +76,7 @@ futures-lite = "1.12"
 epi = { git = "https://github.com/wusyong/egui", branch = "tao", optional = true }
 regex = "1.5"
 glob = "0.3"
+data-url = "0.1"
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
 glib = "0.14"

+ 42 - 20
core/tauri/src/manager.rs

@@ -44,7 +44,10 @@ use std::{
 use tauri_macros::default_runtime;
 use tauri_utils::{
   assets::{AssetKey, CspHash},
-  html::{CSP_TOKEN, INVOKE_KEY_TOKEN, SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
+  html::{
+    inject_csp, parse as parse_html, CSP_TOKEN, INVOKE_KEY_TOKEN, SCRIPT_NONCE_TOKEN,
+    STYLE_NONCE_TOKEN,
+  },
 };
 use url::Url;
 
@@ -271,6 +274,21 @@ impl<R: Runtime> WindowManager<R> {
     key
   }
 
+  fn csp(&self) -> Option<String> {
+    if cfg!(feature = "custom-protocol") {
+      self.inner.config.tauri.security.csp.clone()
+    } else {
+      self
+        .inner
+        .config
+        .tauri
+        .security
+        .dev_csp
+        .clone()
+        .or_else(|| self.inner.config.tauri.security.csp.clone())
+    }
+  }
+
   /// Checks whether the invoke key is valid or not.
   ///
   /// An invoke key is valid if it was generated by this manager instance.
@@ -545,19 +563,7 @@ impl<R: Runtime> WindowManager<R> {
           asset = asset.replacen(INVOKE_KEY_TOKEN, &self.generate_invoke_key().to_string(), 1);
 
           if is_html {
-            let csp = if cfg!(feature = "custom-protocol") {
-              self.inner.config.tauri.security.csp.clone()
-            } else {
-              self
-                .inner
-                .config
-                .tauri
-                .security
-                .dev_csp
-                .clone()
-                .or_else(|| self.inner.config.tauri.security.csp.clone())
-            };
-            if let Some(mut csp) = csp {
+            if let Some(mut csp) = self.csp() {
               let hash_strings = self.inner.assets.csp_hashes(&asset_path).fold(
                 CspHashStrings::default(),
                 |mut acc, hash| {
@@ -764,7 +770,7 @@ impl<R: Runtime> WindowManager<R> {
     if self.windows_lock().contains_key(&pending.label) {
       return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
     }
-    let (is_local, url) = match &pending.webview_attributes.url {
+    let (is_local, mut url) = match &pending.webview_attributes.url {
       WindowUrl::App(path) => {
         let url = self.get_url();
         (
@@ -773,18 +779,32 @@ impl<R: Runtime> WindowManager<R> {
           if path.to_str() != Some("index.html") {
             url
               .join(&*path.to_string_lossy())
-              .map_err(crate::Error::InvalidUrl)?
-              .to_string()
+              .map_err(crate::Error::InvalidUrl)
+              // this will never fail
+              .unwrap()
           } else {
-            url.to_string()
+            url.into_owned()
           },
         )
       }
-      WindowUrl::External(url) => (url.scheme() == "tauri", url.to_string()),
+      WindowUrl::External(url) => (url.scheme() == "tauri", url.clone()),
       _ => unimplemented!(),
     };
 
-    pending.url = url;
+    if let Some(csp) = self.csp() {
+      if url.scheme() == "data" {
+        if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
+          let (body, _) = data_url.decode_to_vec().unwrap();
+          let html = String::from_utf8_lossy(&body).into_owned();
+          // naive way to check if it's an html
+          if html.contains('<') && html.contains('>') {
+            let mut document = parse_html(html);
+            inject_csp(&mut document, &csp);
+            url.set_path(&format!("text/html,{}", document.to_string()));
+          }
+        }
+      }
+    }
 
     if is_local {
       let label = pending.label.clone();
@@ -796,6 +816,8 @@ impl<R: Runtime> WindowManager<R> {
       pending.file_drop_handler = Some(self.prepare_file_drop(app_handle));
     }
 
+    pending.url = url.to_string();
+
     // in `Windows`, we need to force a data_directory
     // but we do respect user-specification
     #[cfg(any(target_os = "linux", target_os = "windows"))]

+ 1 - 1
examples/api/src/components/Window.svelte

@@ -61,7 +61,7 @@
   }
 
   function createWindow() {
-    const label = Math.random().toString();
+    const label = Math.random().toString().replace('.', '');
     const webview = new WebviewWindow(label);
     windowMap[label] = webview;
     webview.once('tauri://error', function () {

+ 2 - 2
examples/multiwindow/index.html

@@ -52,11 +52,11 @@
     var createWindowButton = document.createElement('button')
     createWindowButton.innerHTML = 'Create window'
     createWindowButton.addEventListener('click', function () {
-      var webviewWindow = new WebviewWindow(Math.random().toString())
+      var webviewWindow = new WebviewWindow(Math.random().toString().replace('.', ''))
       webviewWindow.once('tauri://created', function () {
         responseContainer.innerHTML += 'Created new webview'
       })
-      webviewWindow.once('tauri://error', function () {
+      webviewWindow.once('tauri://error', function (e) {
         responseContainer.innerHTML += 'Error creating new webview'
       })
     })

+ 1 - 1
examples/navigation/public/index.js

@@ -12,7 +12,7 @@ document.querySelector('#go').addEventListener('click', () => {
 })
 
 document.querySelector('#open-window').addEventListener('click', () => {
-  new WebviewWindow(Math.random().toString(), {
+  new WebviewWindow(Math.random().toString().replace('.', ''), {
     url: routeSelect.value
   })
 })