diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2024-09-26 02:50:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-26 01:50:54 +0000 |
commit | 5504acea6751480f1425c88353ad5d36257bdce7 (patch) | |
tree | fa02e6c546eae469aac894bfc71600ab4eccad28 /runtime/permissions/lib.rs | |
parent | 05415bb9de475aa8646985a545f30fe93136207e (diff) |
feat: add `--allow-import` flag (#25469)
This replaces `--allow-net` for import permissions and makes the
security sandbox stricter by also checking permissions for statically
analyzable imports.
By default, this has a value of
`--allow-import=deno.land:443,jsr.io:443,esm.sh:443,raw.githubusercontent.com:443,gist.githubusercontent.com:443`,
but that can be overridden by providing a different set of hosts.
Additionally, when no value is provided, import permissions are inferred
from the CLI arguments so the following works because
`fresh.deno.dev:443` will be added to the list of allowed imports:
```ts
deno run -A -r https://fresh.deno.dev
```
---------
Co-authored-by: David Sherret <dsherret@gmail.com>
Diffstat (limited to 'runtime/permissions/lib.rs')
-rw-r--r-- | runtime/permissions/lib.rs | 326 |
1 files changed, 255 insertions, 71 deletions
diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs index c7ef864db..5d8e76125 100644 --- a/runtime/permissions/lib.rs +++ b/runtime/permissions/lib.rs @@ -14,7 +14,6 @@ use deno_core::serde::Deserializer; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::unsync::sync::AtomicFlag; -use deno_core::url; use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_terminal::colors; @@ -950,6 +949,15 @@ impl NetDescriptor { Ok(NetDescriptor(host, port)) } + + pub fn from_url(url: &Url) -> Result<Self, AnyError> { + let host = url + .host_str() + .ok_or_else(|| type_error(format!("Missing host in url: '{}'", url)))?; + let host = Host::parse(host)?; + let port = url.port_or_known_default(); + Ok(NetDescriptor(host, port)) + } } impl fmt::Display for NetDescriptor { @@ -967,6 +975,73 @@ impl fmt::Display for NetDescriptor { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct ImportDescriptor(NetDescriptor); + +impl QueryDescriptor for ImportDescriptor { + type AllowDesc = ImportDescriptor; + type DenyDesc = ImportDescriptor; + + fn flag_name() -> &'static str { + "import" + } + + fn display_name(&self) -> Cow<str> { + self.0.display_name() + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + Self(NetDescriptor::from_allow(&allow.0)) + } + + fn as_allow(&self) -> Option<Self::AllowDesc> { + self.0.as_allow().map(ImportDescriptor) + } + + fn as_deny(&self) -> Self::DenyDesc { + Self(self.0.as_deny()) + } + + fn check_in_permission( + &self, + perm: &mut UnaryPermission<Self>, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + skip_check_if_is_permission_fully_granted!(perm); + perm.check_desc(Some(self), false, api_name) + } + + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.matches_allow(&other.0) + } + + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.matches_deny(&other.0) + } + + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.0.revokes(&other.0) + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.stronger_than_deny(&other.0) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.overlaps_deny(&other.0) + } +} + +impl ImportDescriptor { + pub fn parse(specifier: &str) -> Result<Self, AnyError> { + Ok(ImportDescriptor(NetDescriptor::parse(specifier)?)) + } + + pub fn from_url(url: &Url) -> Result<Self, AnyError> { + Ok(ImportDescriptor(NetDescriptor::from_url(url)?)) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct EnvDescriptor(EnvVarName); impl EnvDescriptor { @@ -1519,19 +1594,35 @@ impl UnaryPermission<NetDescriptor> { self.check_desc(Some(host), false, api_name) } - pub fn check_url( + pub fn check_all(&mut self) -> Result<(), AnyError> { + skip_check_if_is_permission_fully_granted!(self); + self.check_desc(None, false, None) + } +} + +impl UnaryPermission<ImportDescriptor> { + pub fn query(&self, host: Option<&ImportDescriptor>) -> PermissionState { + self.query_desc(host, AllowPartial::TreatAsPartialGranted) + } + + pub fn request( &mut self, - url: &url::Url, + host: Option<&ImportDescriptor>, + ) -> PermissionState { + self.request_desc(host) + } + + pub fn revoke(&mut self, host: Option<&ImportDescriptor>) -> PermissionState { + self.revoke_desc(host) + } + + pub fn check( + &mut self, + host: &ImportDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let host = url - .host_str() - .ok_or_else(|| type_error(format!("Missing host in url: '{}'", url)))?; - let host = Host::parse(host)?; - let port = url.port_or_known_default(); - let descriptor = NetDescriptor(host, port); - self.check_desc(Some(&descriptor), false, api_name) + self.check_desc(Some(host), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { @@ -1700,6 +1791,7 @@ pub struct Permissions { pub sys: UnaryPermission<SysDescriptor>, pub run: UnaryPermission<RunQueryDescriptor>, pub ffi: UnaryPermission<FfiQueryDescriptor>, + pub import: UnaryPermission<ImportDescriptor>, pub all: UnitPermission, } @@ -1720,6 +1812,7 @@ pub struct PermissionsOptions { pub deny_sys: Option<Vec<String>>, pub allow_write: Option<Vec<String>>, pub deny_write: Option<Vec<String>>, + pub allow_import: Option<Vec<String>>, pub prompt: bool, } @@ -1888,6 +1981,13 @@ impl Permissions { })?, opts.prompt, )?, + import: Permissions::new_unary( + parse_maybe_vec(opts.allow_import.as_deref(), |item| { + parser.parse_import_descriptor(item) + })?, + None, + opts.prompt, + )?, all: Permissions::new_all(opts.allow_all), }) } @@ -1902,6 +2002,7 @@ impl Permissions { sys: UnaryPermission::allow_all(), run: UnaryPermission::allow_all(), ffi: UnaryPermission::allow_all(), + import: UnaryPermission::allow_all(), all: Permissions::new_all(true), } } @@ -1925,35 +2026,10 @@ impl Permissions { sys: Permissions::new_unary(None, None, prompt).unwrap(), run: Permissions::new_unary(None, None, prompt).unwrap(), ffi: Permissions::new_unary(None, None, prompt).unwrap(), + import: Permissions::new_unary(None, None, prompt).unwrap(), all: Permissions::new_all(false), } } - - /// A helper function that determines if the module specifier is a local or - /// remote, and performs a read or net check for the specifier. - pub fn check_specifier( - &mut self, - specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - match specifier.scheme() { - "file" => match specifier_to_file_path(specifier) { - Ok(path) => self.read.check( - &PathQueryDescriptor { - requested: path.to_string_lossy().into_owned(), - resolved: path, - } - .into_read(), - Some("import()"), - ), - Err(_) => Err(uri_error(format!( - "Invalid file path.\n Specifier: {specifier}" - ))), - }, - "data" => Ok(()), - "blob" => Ok(()), - _ => self.net.check_url(specifier, Some("import()")), - } - } } /// Attempts to convert a specifier to a file path. By default, uses the Url @@ -2002,6 +2078,12 @@ pub fn specifier_to_file_path( } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum CheckSpecifierKind { + Static, + Dynamic, +} + /// Wrapper struct for `Permissions` that can be shared across threads. /// /// We need a way to have internal mutability for permissions as they might get @@ -2039,8 +2121,43 @@ impl PermissionsContainer { pub fn check_specifier( &self, specifier: &ModuleSpecifier, + kind: CheckSpecifierKind, ) -> Result<(), AnyError> { - self.inner.lock().check_specifier(specifier) + let mut inner = self.inner.lock(); + match specifier.scheme() { + "file" => { + if inner.read.is_allow_all() || kind == CheckSpecifierKind::Static { + return Ok(()); + } + + match specifier_to_file_path(specifier) { + Ok(path) => inner.read.check( + &PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path, + } + .into_read(), + Some("import()"), + ), + Err(_) => Err(uri_error(format!( + "Invalid file path.\n Specifier: {specifier}" + ))), + } + } + "data" => Ok(()), + "blob" => Ok(()), + _ => { + if inner.import.is_allow_all() { + return Ok(()); // avoid allocation below + } + + let desc = self + .descriptor_parser + .parse_import_descriptor_from_url(specifier)?; + inner.import.check(&desc, Some("import()"))?; + Ok(()) + } + } } #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] @@ -2375,7 +2492,12 @@ impl PermissionsContainer { url: &Url, api_name: &str, ) -> Result<(), AnyError> { - self.inner.lock().net.check_url(url, Some(api_name)) + let mut inner = self.inner.lock(); + if inner.net.is_allow_all() { + return Ok(()); + } + let desc = self.descriptor_parser.parse_net_descriptor_from_url(url)?; + inner.net.check(&desc, Some(api_name)) } #[inline(always)] @@ -2589,6 +2711,7 @@ pub struct ChildPermissionsArg { env: ChildUnaryPermissionArg, net: ChildUnaryPermissionArg, ffi: ChildUnaryPermissionArg, + import: ChildUnaryPermissionArg, read: ChildUnaryPermissionArg, run: ChildUnaryPermissionArg, sys: ChildUnaryPermissionArg, @@ -2601,6 +2724,7 @@ impl ChildPermissionsArg { env: ChildUnaryPermissionArg::Inherit, net: ChildUnaryPermissionArg::Inherit, ffi: ChildUnaryPermissionArg::Inherit, + import: ChildUnaryPermissionArg::Inherit, read: ChildUnaryPermissionArg::Inherit, run: ChildUnaryPermissionArg::Inherit, sys: ChildUnaryPermissionArg::Inherit, @@ -2613,6 +2737,7 @@ impl ChildPermissionsArg { env: ChildUnaryPermissionArg::NotGranted, net: ChildUnaryPermissionArg::NotGranted, ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, sys: ChildUnaryPermissionArg::NotGranted, @@ -2677,6 +2802,11 @@ impl<'de> Deserialize<'de> for ChildPermissionsArg { child_permissions_arg.ffi = arg.map_err(|e| { de::Error::custom(format!("(deno.permissions.ffi) {e}")) })?; + } else if key == "import" { + let arg = serde_json::from_value::<ChildUnaryPermissionArg>(value); + child_permissions_arg.import = arg.map_err(|e| { + de::Error::custom(format!("(deno.permissions.import) {e}")) + })?; } else if key == "read" { let arg = serde_json::from_value::<ChildUnaryPermissionArg>(value); child_permissions_arg.read = arg.map_err(|e| { @@ -2726,6 +2856,25 @@ pub trait PermissionDescriptorParser: Debug + Send + Sync { fn parse_net_descriptor(&self, text: &str) -> Result<NetDescriptor, AnyError>; + fn parse_net_descriptor_from_url( + &self, + url: &Url, + ) -> Result<NetDescriptor, AnyError> { + NetDescriptor::from_url(url) + } + + fn parse_import_descriptor( + &self, + text: &str, + ) -> Result<ImportDescriptor, AnyError>; + + fn parse_import_descriptor_from_url( + &self, + url: &Url, + ) -> Result<ImportDescriptor, AnyError> { + ImportDescriptor::from_url(url) + } + fn parse_env_descriptor(&self, text: &str) -> Result<EnvDescriptor, AnyError>; @@ -2785,6 +2934,7 @@ pub fn create_child_permissions( &child_permissions_arg.read, &child_permissions_arg.write, &child_permissions_arg.net, + &child_permissions_arg.import, &child_permissions_arg.env, &child_permissions_arg.sys, &child_permissions_arg.run, @@ -2808,6 +2958,11 @@ pub fn create_child_permissions( .create_child_permissions(child_permissions_arg.write, |text| { Ok(Some(parser.parse_write_descriptor(text)?)) })?; + worker_perms.import = main_perms + .import + .create_child_permissions(child_permissions_arg.import, |text| { + Ok(Some(parser.parse_import_descriptor(text)?)) + })?; worker_perms.net = main_perms .net .create_child_permissions(child_permissions_arg.net, |text| { @@ -2897,6 +3052,13 @@ mod tests { NetDescriptor::parse(text) } + fn parse_import_descriptor( + &self, + text: &str, + ) -> Result<ImportDescriptor, AnyError> { + ImportDescriptor::parse(text) + } + fn parse_env_descriptor( &self, text: &str, @@ -3153,8 +3315,9 @@ mod tests { #[test] fn test_check_net_url() { - let mut perms = Permissions::from_options( - &TestPermissionDescriptorParser, + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, &PermissionsOptions { allow_net: Some(svec![ "localhost", @@ -3168,6 +3331,7 @@ mod tests { }, ) .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); let url_tests = vec![ // Any protocol + port for localhost should be ok, since we don't specify @@ -3209,8 +3373,8 @@ mod tests { ]; for (url_str, is_ok) in url_tests { - let u = url::Url::parse(url_str).unwrap(); - assert_eq!(is_ok, perms.net.check_url(&u, None).is_ok(), "{}", u); + let u = Url::parse(url_str).unwrap(); + assert_eq!(is_ok, perms.check_net_url(&u, "api()").is_ok(), "{}", u); } } @@ -3222,48 +3386,78 @@ mod tests { } else { svec!["/a"] }; - let mut perms = Permissions::from_options( - &TestPermissionDescriptorParser, + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, &PermissionsOptions { allow_read: Some(read_allowlist), - allow_net: Some(svec!["localhost"]), + allow_import: Some(svec!["localhost"]), ..Default::default() }, ) .unwrap(); + let perms = PermissionsContainer::new(Arc::new(parser), perms); let mut fixtures = vec![ ( ModuleSpecifier::parse("http://localhost:4545/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + ), + ( + ModuleSpecifier::parse("http://localhost:4545/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, true, ), ( ModuleSpecifier::parse("http://deno.land/x/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, false, ), ( ModuleSpecifier::parse("data:text/plain,Hello%2C%20Deno!").unwrap(), + CheckSpecifierKind::Dynamic, true, ), ]; if cfg!(target_os = "windows") { - fixtures - .push((ModuleSpecifier::parse("file:///C:/a/mod.ts").unwrap(), true)); + fixtures.push(( + ModuleSpecifier::parse("file:///C:/a/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + true, + )); fixtures.push(( ModuleSpecifier::parse("file:///C:/b/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///C:/b/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, false, )); } else { - fixtures - .push((ModuleSpecifier::parse("file:///a/mod.ts").unwrap(), true)); - fixtures - .push((ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), false)); + fixtures.push(( + ModuleSpecifier::parse("file:///a/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + false, + )); } - for (specifier, expected) in fixtures { + for (specifier, kind, expected) in fixtures { assert_eq!( - perms.check_specifier(&specifier).is_ok(), + perms.check_specifier(&specifier, kind).is_ok(), expected, "{}", specifier, @@ -3272,24 +3466,6 @@ mod tests { } #[test] - fn check_invalid_specifiers() { - set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::allow_all(); - - let mut test_cases = vec![]; - - test_cases.push("file://dir"); - test_cases.push("file://asdf/"); - test_cases.push("file://remotehost/"); - - for url in test_cases { - assert!(perms - .check_specifier(&ModuleSpecifier::parse(url).unwrap()) - .is_err()); - } - } - - #[test] fn test_query() { set_prompter(Box::new(TestPrompter)); let parser = TestPermissionDescriptorParser; @@ -3885,6 +4061,7 @@ mod tests { env: ChildUnaryPermissionArg::Inherit, net: ChildUnaryPermissionArg::Inherit, ffi: ChildUnaryPermissionArg::Inherit, + import: ChildUnaryPermissionArg::Inherit, read: ChildUnaryPermissionArg::Inherit, run: ChildUnaryPermissionArg::Inherit, sys: ChildUnaryPermissionArg::Inherit, @@ -3897,6 +4074,7 @@ mod tests { env: ChildUnaryPermissionArg::NotGranted, net: ChildUnaryPermissionArg::NotGranted, ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, sys: ChildUnaryPermissionArg::NotGranted, @@ -3930,6 +4108,7 @@ mod tests { "env": true, "net": true, "ffi": true, + "import": true, "read": true, "run": true, "sys": true, @@ -3940,6 +4119,7 @@ mod tests { env: ChildUnaryPermissionArg::Granted, net: ChildUnaryPermissionArg::Granted, ffi: ChildUnaryPermissionArg::Granted, + import: ChildUnaryPermissionArg::Granted, read: ChildUnaryPermissionArg::Granted, run: ChildUnaryPermissionArg::Granted, sys: ChildUnaryPermissionArg::Granted, @@ -3951,6 +4131,7 @@ mod tests { "env": false, "net": false, "ffi": false, + "import": false, "read": false, "run": false, "sys": false, @@ -3961,6 +4142,7 @@ mod tests { env: ChildUnaryPermissionArg::NotGranted, net: ChildUnaryPermissionArg::NotGranted, ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, sys: ChildUnaryPermissionArg::NotGranted, @@ -3972,6 +4154,7 @@ mod tests { "env": ["foo", "bar"], "net": ["foo", "bar:8000"], "ffi": ["foo", "file:///bar/baz"], + "import": ["example.com"], "read": ["foo", "file:///bar/baz"], "run": ["foo", "file:///bar/baz", "./qux"], "sys": ["hostname", "osRelease"], @@ -3985,6 +4168,7 @@ mod tests { "foo", "file:///bar/baz" ]), + import: ChildUnaryPermissionArg::GrantedList(svec!["example.com"]), read: ChildUnaryPermissionArg::GrantedList(svec![ "foo", "file:///bar/baz" |