Browse Source

feat(core): allow denying paths on the fs and asset scopes (#3607)

Lucas Fernandes Nogueira 3 năm trước cách đây
mục cha
commit
983ccb815b

+ 5 - 0
.changes/fs-scope-forbidden-paths.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Allow configuring forbidden paths on the asset and filesystem scopes.

+ 53 - 4
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<PathBuf>);
+#[serde(untagged)]
+pub enum FsAllowlistScope {
+  /// A list of paths that are allowed by this scope.
+  AllowedPaths(Vec<PathBuf>),
+  /// A complete scope configuration.
+  Scope {
+    /// A list of paths that are allowed by this scope.
+    #[serde(default)]
+    allow: Vec<PathBuf>,
+    /// A list of paths that are not allowed by this scope.
+    /// This gets precedence over the [`Self::allow`] list.
+    #[serde(default)]
+    deny: Vec<PathBuf>,
+  },
+}
+
+impl Default for FsAllowlistScope {
+  fn default() -> Self {
+    Self::AllowedPaths(Vec::new())
+  }
+}
+
+impl FsAllowlistScope {
+  /// The list of allowed paths.
+  pub fn allowed_paths(&self) -> &Vec<PathBuf> {
+    match self {
+      Self::AllowedPaths(p) => p,
+      Self::Scope { allow, .. } => allow,
+    }
+  }
+
+  /// The list of forbidden paths.
+  pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
+    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 } }
+        }
+      });
     }
   }
 

+ 58 - 4
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<Mutex<Vec<Pattern>>>,
+  forbidden_patterns: Arc<Mutex<Vec<Pattern>>>,
 }
 
 impl fmt::Debug for Scope {
@@ -35,6 +36,16 @@ impl fmt::Debug for Scope {
           .map(|p| p.as_str())
           .collect::<Vec<&str>>(),
       )
+      .field(
+        "forbidden_patterns",
+        &self
+          .forbidden_patterns
+          .lock()
+          .unwrap()
+          .iter()
+          .map(|p| p.as_str())
+          .collect::<Vec<&str>>(),
+      )
       .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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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
     }

+ 9 - 3
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": {

+ 2 - 2
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",
 ]

+ 31 - 4
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.",