Skip to content

Commit

Permalink
feat: allow limiting dangerousDisableAssetCspModification, closes #3831
Browse files Browse the repository at this point in the history
… (#4021)
  • Loading branch information
lucasfernog committed May 3, 2022
1 parent a6f1734 commit 164078c
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 71 deletions.
7 changes: 7 additions & 0 deletions .changes/dangerous-disable-asset-csp-modification-config.md
@@ -0,0 +1,7 @@
---
"tauri": patch
"tauri-utils": patch
"tauri-codegen": patch
---

The `dangerous_allow_asset_csp_modification` configuration value has been changed to allow a list of CSP directives to disable.
57 changes: 32 additions & 25 deletions core/tauri-codegen/src/context.rs
Expand Up @@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};

use tauri_utils::assets::AssetKey;
use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
use tauri_utils::html::{inject_nonce_token, parse as parse_html, NodeRef};
use tauri_utils::html::{inject_nonce_token, parse as parse_html};

#[cfg(feature = "shell-scope")]
use tauri_utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistScope};
Expand All @@ -26,32 +26,14 @@ pub struct ContextData {
pub root: TokenStream,
}

fn load_csp(document: &mut NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
inject_nonce_token(document);
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
let mut scripts = Vec::new();
for inline_script_el in inline_script_elements {
let script = inline_script_el.as_node().text_contents();
let mut hasher = Sha256::new();
hasher.update(&script);
let hash = hasher.finalize();
scripts.push(format!("'sha256-{}'", base64::encode(&hash)));
}
csp_hashes
.inline_scripts
.entry(key.clone().into())
.or_default()
.append(&mut scripts);
}
}

fn map_core_assets(
options: &AssetOptions,
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
#[cfg(feature = "isolation")]
let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
let csp = options.csp;
let dangerous_disable_asset_csp_modification = options.dangerous_disable_asset_csp_modification;
let dangerous_disable_asset_csp_modification =
options.dangerous_disable_asset_csp_modification.clone();
move |key, path, input, csp_hashes| {
if path.extension() == Some(OsStr::new("html")) {
let mut document = parse_html(String::from_utf8_lossy(input).into_owned());
Expand All @@ -61,10 +43,28 @@ fn map_core_assets(
#[cfg(target_os = "linux")]
::tauri_utils::html::inject_csp_token(&mut document);

if !dangerous_disable_asset_csp_modification {
load_csp(&mut document, key, csp_hashes);
inject_nonce_token(&mut document, &dangerous_disable_asset_csp_modification);

if dangerous_disable_asset_csp_modification.can_modify("script-src") {
if let Ok(inline_script_elements) = document.select("script:not(empty)") {
let mut scripts = Vec::new();
for inline_script_el in inline_script_elements {
let script = inline_script_el.as_node().text_contents();
let mut hasher = Sha256::new();
hasher.update(&script);
let hash = hasher.finalize();
scripts.push(format!("'sha256-{}'", base64::encode(&hash)));
}
csp_hashes
.inline_scripts
.entry(key.clone().into())
.or_default()
.append(&mut scripts);
}
}

#[cfg(feature = "isolation")]
#[cfg(feature = "isolation")]
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
// create the csp for the isolation iframe styling now, to make the runtime less complex
let mut hasher = Sha256::new();
Expand Down Expand Up @@ -116,7 +116,14 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
} = data;

let mut options = AssetOptions::new(config.tauri.pattern.clone())
.freeze_prototype(config.tauri.security.freeze_prototype);
.freeze_prototype(config.tauri.security.freeze_prototype)
.dangerous_disable_asset_csp_modification(
config
.tauri
.security
.dangerous_disable_asset_csp_modification
.clone(),
);
let csp = if dev {
config
.tauri
Expand Down
44 changes: 25 additions & 19 deletions core/tauri-codegen/src/embedded_assets.rs
Expand Up @@ -11,8 +11,8 @@ use std::{
fs::File,
path::{Path, PathBuf},
};
use tauri_utils::assets::AssetKey;
use tauri_utils::config::PatternKind;
use tauri_utils::{assets::AssetKey, config::DisabledCspModificationKind};
use thiserror::Error;
use walkdir::{DirEntry, WalkDir};

Expand Down Expand Up @@ -123,9 +123,9 @@ impl RawEmbeddedAssets {

// compress all files encountered
Ok(entry) => {
if options.dangerous_disable_asset_csp_modification {
Some(Ok((prefix, entry)))
} else if let Err(error) = csp_hashes.add_if_applicable(&entry) {
if let Err(error) = csp_hashes
.add_if_applicable(&entry, &options.dangerous_disable_asset_csp_modification)
{
Some(Err(error))
} else {
Some(Ok((prefix, entry)))
Expand Down Expand Up @@ -160,22 +160,28 @@ impl CspHashes {
///
/// Note: this only checks the file extension, much like how a browser will assume a .js file is
/// a JavaScript file unless HTTP headers tell it otherwise.
pub fn add_if_applicable(&mut self, entry: &DirEntry) -> Result<(), EmbeddedAssetsError> {
pub fn add_if_applicable(
&mut self,
entry: &DirEntry,
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
) -> Result<(), EmbeddedAssetsError> {
let path = entry.path();

// we only hash JavaScript files for now, may expand to other CSP hashable types in the future
if let Some("js") | Some("mjs") = path.extension().and_then(|os| os.to_str()) {
let mut hasher = Sha256::new();
hasher.update(
&std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
path: path.to_path_buf(),
error,
})?,
);
let hash = hasher.finalize();
self
.scripts
.push(format!("'sha256-{}'", base64::encode(hash)))
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
let mut hasher = Sha256::new();
hasher.update(
&std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
path: path.to_path_buf(),
error,
})?,
);
let hash = hasher.finalize();
self
.scripts
.push(format!("'sha256-{}'", base64::encode(hash)));
}
}

Ok(())
Expand All @@ -188,7 +194,7 @@ pub struct AssetOptions {
pub(crate) csp: bool,
pub(crate) pattern: PatternKind,
pub(crate) freeze_prototype: bool,
pub(crate) dangerous_disable_asset_csp_modification: bool,
pub(crate) dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
#[cfg(feature = "isolation")]
pub(crate) isolation_schema: String,
}
Expand All @@ -200,7 +206,7 @@ impl AssetOptions {
csp: false,
pattern,
freeze_prototype: false,
dangerous_disable_asset_csp_modification: false,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
#[cfg(feature = "isolation")]
isolation_schema: format!("isolation-{}", uuid::Uuid::new_v4()),
}
Expand All @@ -223,7 +229,7 @@ impl AssetOptions {
/// Instruct the asset handler to **NOT** modify the CSP. This is **NOT** recommended.
pub fn dangerous_disable_asset_csp_modification(
mut self,
dangerous_disable_asset_csp_modification: bool,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
) -> Self {
self.dangerous_disable_asset_csp_modification = dangerous_disable_asset_csp_modification;
self
Expand Down
54 changes: 51 additions & 3 deletions core/tauri-utils/src/config.rs
Expand Up @@ -757,6 +757,34 @@ impl Display for Csp {
}
}

/// The possible values for the `dangerous_disable_asset_csp_modification` config option.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum DisabledCspModificationKind {
/// If `true`, disables all CSP modification.
/// `false` is the default value and it configures Tauri to control the CSP.
Flag(bool),
/// Disables the given list of CSP directives modifications.
List(Vec<String>),
}

impl DisabledCspModificationKind {
/// Determines whether the given CSP directive can be modified or not.
pub fn can_modify(&self, directive: &str) -> bool {
match self {
Self::Flag(f) => !f,
Self::List(l) => !l.contains(&directive.into()),
}
}
}

impl Default for DisabledCspModificationKind {
fn default() -> Self {
Self::Flag(false)
}
}

/// Security configuration.
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
Expand All @@ -783,10 +811,14 @@ pub struct SecurityConfig {
/// to only allow loading of your own scripts and styles by injecting nonce and hash sources.
/// This stricts your CSP, which may introduce issues when using along with other flexing sources.
///
/// This configuration option allows both a boolean and a list of strings as value.
/// A boolean instructs Tauri to disable the injection for all CSP injections,
/// and a list of strings indicates the CSP directives that Tauri cannot inject.
///
/// **WARNING:** Only disable this if you know what you are doing and have properly configured the CSP.
/// Your application might be vulnerable to XSS attacks without this Tauri protection.
#[serde(default)]
pub dangerous_disable_asset_csp_modification: bool,
pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
}

/// Defines an allowlist type.
Expand Down Expand Up @@ -2637,12 +2669,28 @@ mod build {
}
}

impl ToTokens for DisabledCspModificationKind {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::config::DisabledCspModificationKind };

tokens.append_all(match self {
Self::Flag(flag) => {
quote! { #prefix::Flag(#flag) }
}
Self::List(directives) => {
let directives = vec_lit(directives, str_lit);
quote! { #prefix::List(#directives) }
}
});
}
}

impl ToTokens for SecurityConfig {
fn to_tokens(&self, tokens: &mut TokenStream) {
let csp = opt_lit(self.csp.as_ref());
let dev_csp = opt_lit(self.dev_csp.as_ref());
let freeze_prototype = self.freeze_prototype;
let dangerous_disable_asset_csp_modification = self.dangerous_disable_asset_csp_modification;
let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;

literal_struct!(
tokens,
Expand Down Expand Up @@ -2901,7 +2949,7 @@ mod test {
csp: None,
dev_csp: None,
freeze_prototype: false,
dangerous_disable_asset_csp_modification: false,
dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
},
allowlist: AllowlistConfig::default(),
system_tray: None,
Expand Down
15 changes: 11 additions & 4 deletions core/tauri-utils/src/html.rs
Expand Up @@ -13,7 +13,7 @@ use serde::Serialize;
#[cfg(feature = "isolation")]
use serialize_to_javascript::DefaultTemplate;

use crate::config::PatternKind;
use crate::config::{DisabledCspModificationKind, PatternKind};
#[cfg(feature = "isolation")]
use crate::pattern::isolation::IsolationJavascriptCodegen;

Expand Down Expand Up @@ -59,9 +59,16 @@ fn inject_nonce(document: &mut NodeRef, selector: &str, token: &str) {
}

/// Inject nonce tokens to all scripts and styles.
pub fn inject_nonce_token(document: &mut NodeRef) {
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
pub fn inject_nonce_token(
document: &mut NodeRef,
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
) {
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
}
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
}
}

/// Injects a content security policy to the HTML.
Expand Down
41 changes: 25 additions & 16 deletions core/tauri/src/manager.rs
Expand Up @@ -79,7 +79,7 @@ fn set_csp<R: Runtime>(
asset: &mut String,
assets: Arc<dyn Assets>,
asset_path: &AssetKey,
#[allow(unused_variables)] manager: &WindowManager<R>,
manager: &WindowManager<R>,
csp: Csp,
) -> String {
let mut csp = csp.into();
Expand All @@ -103,21 +103,30 @@ fn set_csp<R: Runtime>(
acc
});

replace_csp_nonce(
asset,
SCRIPT_NONCE_TOKEN,
&mut csp,
"script-src",
hash_strings.script,
);

replace_csp_nonce(
asset,
STYLE_NONCE_TOKEN,
&mut csp,
"style-src",
hash_strings.style,
);
let dangerous_disable_asset_csp_modification = &manager
.config()
.tauri
.security
.dangerous_disable_asset_csp_modification;
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
replace_csp_nonce(
asset,
SCRIPT_NONCE_TOKEN,
&mut csp,
"script-src",
hash_strings.script,
);
}

if dangerous_disable_asset_csp_modification.can_modify("style-src") {
replace_csp_nonce(
asset,
STYLE_NONCE_TOKEN,
&mut csp,
"style-src",
hash_strings.style,
);
}

#[cfg(feature = "isolation")]
if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
Expand Down
4 changes: 2 additions & 2 deletions examples/api/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 164078c

Please sign in to comment.