Skip to content

Commit 8259cd6

Browse files
committed
feat(core): inject CSP on data URLs [TRI-049] (#16)
1 parent d4017d5 commit 8259cd6

9 files changed

Lines changed: 69 additions & 33 deletions

File tree

.changes/data-url-csp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Inject configured `CSP` on `data:` URLs.

core/tauri-codegen/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ tauri-utils = { version = "1.0.0-beta.3", path = "../tauri-utils", features = [
2424
thiserror = "1"
2525
walkdir = "2"
2626
zstd = { version = "0.9", optional = true }
27-
kuchiki = "0.8"
2827
regex = "1"
2928

3029
[features]

core/tauri-codegen/src/embedded_assets.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use kuchiki::traits::*;
65
use proc_macro2::TokenStream;
76
use quote::{quote, ToTokens, TokenStreamExt};
87
use regex::RegexSet;
@@ -15,7 +14,7 @@ use std::{
1514
};
1615
use tauri_utils::{
1716
assets::AssetKey,
18-
html::{inject_invoke_key_token, inject_nonce_token},
17+
html::{inject_invoke_key_token, inject_nonce_token, parse as parse_html},
1918
};
2019
use thiserror::Error;
2120
use walkdir::{DirEntry, WalkDir};
@@ -253,7 +252,7 @@ impl EmbeddedAssets {
253252
})?;
254253

255254
if path.extension() == Some(OsStr::new("html")) {
256-
let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&input).into_owned());
255+
let mut document = parse_html(String::from_utf8_lossy(&input).into_owned());
257256
if options.csp {
258257
#[cfg(target_os = "linux")]
259258
::tauri_utils::html::inject_csp_token(&mut document);

core/tauri-utils/src/html.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
//! The module to process HTML in Tauri.
66
7-
use html5ever::{interface::QualName, namespace_url, ns, LocalName};
7+
use html5ever::{interface::QualName, namespace_url, ns, tendril::TendrilSink, LocalName};
88
use kuchiki::{Attribute, ExpandedName, NodeRef};
99

1010
/// The token used on the CSP tag content.
@@ -16,6 +16,11 @@ pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
1616
/// The token used for the invoke key.
1717
pub const INVOKE_KEY_TOKEN: &str = "__TAURI__INVOKE_KEY_TOKEN__";
1818

19+
/// Parses the given HTML string.
20+
pub fn parse(html: String) -> NodeRef {
21+
kuchiki::parse_html().one(html)
22+
}
23+
1924
fn inject_nonce(document: &mut NodeRef, selector: &str, token: &str) {
2025
if let Ok(scripts) = document.select(selector) {
2126
for target in scripts {
@@ -108,20 +113,25 @@ pub fn inject_invoke_key_token(document: &mut NodeRef) {
108113
}
109114
}
110115

111-
/// Injects a content security policy token to the HTML.
112-
pub fn inject_csp_token(document: &mut NodeRef) {
116+
/// Injects a content security policy to the HTML.
117+
pub fn inject_csp(document: &mut NodeRef, csp: &str) {
113118
if let Ok(ref head) = document.select_first("head") {
114-
head.as_node().append(create_csp_meta_tag(CSP_TOKEN));
119+
head.as_node().append(create_csp_meta_tag(csp));
115120
} else {
116121
let head = NodeRef::new_element(
117122
QualName::new(None, ns!(html), LocalName::from("head")),
118123
None,
119124
);
120-
head.append(create_csp_meta_tag(CSP_TOKEN));
125+
head.append(create_csp_meta_tag(csp));
121126
document.prepend(head);
122127
}
123128
}
124129

130+
/// Injects a content security policy token to the HTML.
131+
pub fn inject_csp_token(document: &mut NodeRef) {
132+
inject_csp(document, CSP_TOKEN)
133+
}
134+
125135
fn create_csp_meta_tag(csp: &str) -> NodeRef {
126136
NodeRef::new_element(
127137
QualName::new(None, ns!(html), LocalName::from("meta")),

core/tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ futures-lite = "1.12"
7676
epi = { git = "https://github.com/wusyong/egui", branch = "tao", optional = true }
7777
regex = "1.5"
7878
glob = "0.3"
79+
data-url = "0.1"
7980

8081
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
8182
glib = "0.14"

core/tauri/src/manager.rs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ use std::{
4444
use tauri_macros::default_runtime;
4545
use tauri_utils::{
4646
assets::{AssetKey, CspHash},
47-
html::{CSP_TOKEN, INVOKE_KEY_TOKEN, SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
47+
html::{
48+
inject_csp, parse as parse_html, CSP_TOKEN, INVOKE_KEY_TOKEN, SCRIPT_NONCE_TOKEN,
49+
STYLE_NONCE_TOKEN,
50+
},
4851
};
4952
use url::Url;
5053

@@ -271,6 +274,21 @@ impl<R: Runtime> WindowManager<R> {
271274
key
272275
}
273276

277+
fn csp(&self) -> Option<String> {
278+
if cfg!(feature = "custom-protocol") {
279+
self.inner.config.tauri.security.csp.clone()
280+
} else {
281+
self
282+
.inner
283+
.config
284+
.tauri
285+
.security
286+
.dev_csp
287+
.clone()
288+
.or_else(|| self.inner.config.tauri.security.csp.clone())
289+
}
290+
}
291+
274292
/// Checks whether the invoke key is valid or not.
275293
///
276294
/// An invoke key is valid if it was generated by this manager instance.
@@ -545,19 +563,7 @@ impl<R: Runtime> WindowManager<R> {
545563
asset = asset.replacen(INVOKE_KEY_TOKEN, &self.generate_invoke_key().to_string(), 1);
546564

547565
if is_html {
548-
let csp = if cfg!(feature = "custom-protocol") {
549-
self.inner.config.tauri.security.csp.clone()
550-
} else {
551-
self
552-
.inner
553-
.config
554-
.tauri
555-
.security
556-
.dev_csp
557-
.clone()
558-
.or_else(|| self.inner.config.tauri.security.csp.clone())
559-
};
560-
if let Some(mut csp) = csp {
566+
if let Some(mut csp) = self.csp() {
561567
let hash_strings = self.inner.assets.csp_hashes(&asset_path).fold(
562568
CspHashStrings::default(),
563569
|mut acc, hash| {
@@ -764,7 +770,7 @@ impl<R: Runtime> WindowManager<R> {
764770
if self.windows_lock().contains_key(&pending.label) {
765771
return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
766772
}
767-
let (is_local, url) = match &pending.webview_attributes.url {
773+
let (is_local, mut url) = match &pending.webview_attributes.url {
768774
WindowUrl::App(path) => {
769775
let url = self.get_url();
770776
(
@@ -773,18 +779,32 @@ impl<R: Runtime> WindowManager<R> {
773779
if path.to_str() != Some("index.html") {
774780
url
775781
.join(&*path.to_string_lossy())
776-
.map_err(crate::Error::InvalidUrl)?
777-
.to_string()
782+
.map_err(crate::Error::InvalidUrl)
783+
// this will never fail
784+
.unwrap()
778785
} else {
779-
url.to_string()
786+
url.into_owned()
780787
},
781788
)
782789
}
783-
WindowUrl::External(url) => (url.scheme() == "tauri", url.to_string()),
790+
WindowUrl::External(url) => (url.scheme() == "tauri", url.clone()),
784791
_ => unimplemented!(),
785792
};
786793

787-
pending.url = url;
794+
if let Some(csp) = self.csp() {
795+
if url.scheme() == "data" {
796+
if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
797+
let (body, _) = data_url.decode_to_vec().unwrap();
798+
let html = String::from_utf8_lossy(&body).into_owned();
799+
// naive way to check if it's an html
800+
if html.contains('<') && html.contains('>') {
801+
let mut document = parse_html(html);
802+
inject_csp(&mut document, &csp);
803+
url.set_path(&format!("text/html,{}", document.to_string()));
804+
}
805+
}
806+
}
807+
}
788808

789809
if is_local {
790810
let label = pending.label.clone();
@@ -796,6 +816,8 @@ impl<R: Runtime> WindowManager<R> {
796816
pending.file_drop_handler = Some(self.prepare_file_drop(app_handle));
797817
}
798818

819+
pending.url = url.to_string();
820+
799821
// in `Windows`, we need to force a data_directory
800822
// but we do respect user-specification
801823
#[cfg(any(target_os = "linux", target_os = "windows"))]

examples/api/src/components/Window.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
}
6262
6363
function createWindow() {
64-
const label = Math.random().toString();
64+
const label = Math.random().toString().replace('.', '');
6565
const webview = new WebviewWindow(label);
6666
windowMap[label] = webview;
6767
webview.once('tauri://error', function () {

examples/multiwindow/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252
var createWindowButton = document.createElement('button')
5353
createWindowButton.innerHTML = 'Create window'
5454
createWindowButton.addEventListener('click', function () {
55-
var webviewWindow = new WebviewWindow(Math.random().toString())
55+
var webviewWindow = new WebviewWindow(Math.random().toString().replace('.', ''))
5656
webviewWindow.once('tauri://created', function () {
5757
responseContainer.innerHTML += 'Created new webview'
5858
})
59-
webviewWindow.once('tauri://error', function () {
59+
webviewWindow.once('tauri://error', function (e) {
6060
responseContainer.innerHTML += 'Error creating new webview'
6161
})
6262
})

examples/navigation/public/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ document.querySelector('#go').addEventListener('click', () => {
1212
})
1313

1414
document.querySelector('#open-window').addEventListener('click', () => {
15-
new WebviewWindow(Math.random().toString(), {
15+
new WebviewWindow(Math.random().toString().replace('.', ''), {
1616
url: routeSelect.value
1717
})
1818
})

0 commit comments

Comments
 (0)