diff --git a/.changes/fs-scope-forbidden-paths.md b/.changes/fs-scope-forbidden-paths.md new file mode 100644 index 000000000..af6dae006 --- /dev/null +++ b/.changes/fs-scope-forbidden-paths.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Allow configuring forbidden paths on the asset and filesystem scopes. diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index e82d9e777..4d6162d29 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -638,9 +638,47 @@ macro_rules! check_feature { /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`. -#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] -pub struct FsAllowlistScope(pub Vec); +#[serde(untagged)] +pub enum FsAllowlistScope { + /// A list of paths that are allowed by this scope. + AllowedPaths(Vec), + /// A complete scope configuration. + Scope { + /// A list of paths that are allowed by this scope. + #[serde(default)] + allow: Vec, + /// A list of paths that are not allowed by this scope. + /// This gets precedence over the [`Self::allow`] list. + #[serde(default)] + deny: Vec, + }, +} + +impl Default for FsAllowlistScope { + fn default() -> Self { + Self::AllowedPaths(Vec::new()) + } +} + +impl FsAllowlistScope { + /// The list of allowed paths. + pub fn allowed_paths(&self) -> &Vec { + match self { + Self::AllowedPaths(p) => p, + Self::Scope { allow, .. } => allow, + } + } + + /// The list of forbidden paths. + pub fn forbidden_paths(&self) -> Option<&Vec> { + match self { + Self::AllowedPaths(_) => None, + Self::Scope { deny, .. } => Some(deny), + } + } +} /// Allowlist for the file system APIs. #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] @@ -2381,8 +2419,19 @@ mod build { impl ToTokens for FsAllowlistScope { fn to_tokens(&self, tokens: &mut TokenStream) { - let allowed_paths = vec_lit(&self.0, path_buf_lit); - tokens.append_all(quote! { ::tauri::utils::config::FsAllowlistScope(#allowed_paths) }) + let prefix = quote! { ::tauri::utils::config::FsAllowlistScope }; + + tokens.append_all(match self { + Self::AllowedPaths(allow) => { + let allowed_paths = vec_lit(allow, path_buf_lit); + quote! { #prefix::AllowedPaths(#allowed_paths) } + } + Self::Scope { allow, deny } => { + let allow = vec_lit(allow, path_buf_lit); + let deny = vec_lit(deny, path_buf_lit); + quote! { #prefix::Scope { allow: #allow, deny: #deny } } + } + }); } } diff --git a/core/tauri/src/scope/fs.rs b/core/tauri/src/scope/fs.rs index 36e5cc851..15d95e36a 100644 --- a/core/tauri/src/scope/fs.rs +++ b/core/tauri/src/scope/fs.rs @@ -20,6 +20,7 @@ use crate::api::path::parse as parse_path; #[derive(Clone)] pub struct Scope { allow_patterns: Arc>>, + forbidden_patterns: Arc>>, } impl fmt::Debug for Scope { @@ -35,6 +36,16 @@ impl fmt::Debug for Scope { .map(|p| p.as_str()) .collect::>(), ) + .field( + "forbidden_patterns", + &self + .forbidden_patterns + .lock() + .unwrap() + .iter() + .map(|p| p.as_str()) + .collect::>(), + ) .finish() } } @@ -58,13 +69,24 @@ impl Scope { scope: &FsAllowlistScope, ) -> Self { let mut allow_patterns = Vec::new(); - for path in &scope.0 { + for path in scope.allowed_paths() { if let Ok(path) = parse_path(config, package_info, env, path) { push_pattern(&mut allow_patterns, path); } } + + let mut forbidden_patterns = Vec::new(); + if let Some(forbidden_paths) = scope.forbidden_paths() { + for path in forbidden_paths { + if let Ok(path) = parse_path(config, package_info, env, path) { + push_pattern(&mut forbidden_patterns, path); + } + } + } + Self { allow_patterns: Arc::new(Mutex::new(allow_patterns)), + forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)), } } @@ -89,6 +111,26 @@ impl Scope { push_pattern(&mut self.allow_patterns.lock().unwrap(), path); } + /// Set the given directory path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_directory>(&self, path: P, recursive: bool) { + let path = path.as_ref().to_path_buf(); + let mut list = self.forbidden_patterns.lock().unwrap(); + + // allow the directory to be read + push_pattern(&mut list, &path); + // allow its files and subdirectories to be read + push_pattern(&mut list, path.join(if recursive { "**" } else { "*" })); + } + + /// Set the given file path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_file>(&self, path: P) { + push_pattern(&mut self.forbidden_patterns.lock().unwrap(), path); + } + /// Determines if the given path is allowed on this scope. pub fn is_allowed>(&self, path: P) -> bool { let path = path.as_ref(); @@ -100,13 +142,25 @@ impl Scope { if let Ok(path) = path { let path: PathBuf = path.components().collect(); - let allowed = self - .allow_patterns + + let forbidden = self + .forbidden_patterns .lock() .unwrap() .iter() .any(|p| p.matches_path(&path)); - allowed + + if forbidden { + false + } else { + let allowed = self + .allow_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path(&path)); + allowed + } } else { false } diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index 81ccf871d..896531537 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -85,7 +85,10 @@ "allowlist": { "all": true, "fs": { - "scope": ["$APP/db", "$DOWNLOAD/**", "$RESOURCE/**"] + "scope": { + "allow": ["$APP/db/**", "$DOWNLOAD/**", "$RESOURCE/**"], + "deny": ["$APP/db/*.stronghold"] + } }, "shell": { "scope": [ @@ -103,7 +106,10 @@ }, "protocol": { "asset": true, - "assetScope": ["$RESOURCE/**", "$APP/**"] + "assetScope": { + "allow": ["$APP/db/**", "$RESOURCE/**"], + "deny": ["$APP/db/*.stronghold"] + } }, "http": { "scope": ["https://jsonplaceholder.typicode.com/todos/*"] @@ -116,7 +122,7 @@ } ], "security": { - "csp": "default-src 'self' customprotocol: img-src: 'self'; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com; img-src 'self' asset: https://asset.localhost blob: data:; font-src https://fonts.gstatic.com", + "csp": "default-src 'self' customprotocol: asset: img-src: 'self'; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com; img-src 'self' asset: https://asset.localhost blob: data:; font-src https://fonts.gstatic.com", "freezePrototype": true }, "systemTray": { diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index b3ec30a45..8069dae60 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -2756,9 +2756,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 1cc563ee3..512da62cb 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -997,10 +997,37 @@ }, "FsAllowlistScope": { "description": "Filesystem scope definition. It is a list of glob patterns that restrict the API access from the webview.\n\nEach pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`.", - "type": "array", - "items": { - "type": "string" - } + "anyOf": [ + { + "description": "A list of paths that are allowed by this scope.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "A complete scope configuration.", + "type": "object", + "properties": { + "allow": { + "description": "A list of paths that are allowed by this scope.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "A list of paths that are not allowed by this scope. This gets precedence over the [`Self::allow`] list.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] }, "GlobalShortcutAllowlistConfig": { "description": "Allowlist for the global shortcut APIs.",