Skip to content

Commit

Permalink
feat(core): inject CSP on data URLs [TRI-049] (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Jan 9, 2022
1 parent d4017d5 commit 8259cd6
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changes/data-url-csp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Inject configured `CSP` on `data:` URLs.
1 change: 0 additions & 1 deletion core/tauri-codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 2 additions & 3 deletions core/tauri-codegen/src/embedded_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 15 additions & 5 deletions core/tauri-utils/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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")),
Expand Down
1 change: 1 addition & 0 deletions core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
62 changes: 42 additions & 20 deletions core/tauri/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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();
(
Expand All @@ -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();
Expand All @@ -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"))]
Expand Down
2 changes: 1 addition & 1 deletion examples/api/src/components/Window.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
4 changes: 2 additions & 2 deletions examples/multiwindow/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
})
Expand Down
2 changes: 1 addition & 1 deletion examples/navigation/public/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})

0 comments on commit 8259cd6

Please sign in to comment.