summaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-04-19 18:12:03 -0600
committerGitHub <noreply@github.com>2024-04-19 18:12:03 -0600
commit472a37064071c66cd1311cdea2e78de8d2bc0641 (patch)
tree94459f249eee0429480e2cea6ac37319e27de41d /runtime
parent365e1f48f7059f94d4eeb8f5ba8b3949b686b355 (diff)
feat(runtime): Allow embedders to perform additional access checks on file open (#23208)
Embedders may have special requirements around file opening, so we add a new `check_open` permission check that is called as part of the file open process.
Diffstat (limited to 'runtime')
-rw-r--r--runtime/permissions.rs30
-rw-r--r--runtime/permissions/lib.rs87
-rw-r--r--runtime/snapshot.rs12
3 files changed, 128 insertions, 1 deletions
diff --git a/runtime/permissions.rs b/runtime/permissions.rs
index ccd0d3254..edd03e1d5 100644
--- a/runtime/permissions.rs
+++ b/runtime/permissions.rs
@@ -1,9 +1,11 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+use std::borrow::Cow;
use std::path::Path;
use deno_core::error::AnyError;
use deno_core::url::Url;
+pub use deno_io::fs::FsError;
pub use deno_permissions::create_child_permissions;
pub use deno_permissions::parse_sys_kind;
pub use deno_permissions::set_prompt_callbacks;
@@ -142,6 +144,34 @@ impl deno_websocket::WebSocketPermissions for PermissionsContainer {
}
impl deno_fs::FsPermissions for PermissionsContainer {
+ fn check_open<'a>(
+ &mut self,
+ resolved: bool,
+ read: bool,
+ write: bool,
+ path: &'a Path,
+ api_name: &str,
+ ) -> Result<Cow<'a, Path>, FsError> {
+ if resolved {
+ self.check_special_file(path, api_name).map_err(|_| {
+ std::io::Error::from(std::io::ErrorKind::PermissionDenied)
+ })?;
+ return Ok(Cow::Borrowed(path));
+ }
+
+ // If somehow read or write aren't specified, use read
+ let read = read || !write;
+ if read {
+ deno_fs::FsPermissions::check_read(self, path, api_name)
+ .map_err(|_| FsError::PermissionDenied("read"))?;
+ }
+ if write {
+ deno_fs::FsPermissions::check_write(self, path, api_name)
+ .map_err(|_| FsError::PermissionDenied("write"))?;
+ }
+ Ok(Cow::Borrowed(path))
+ }
+
fn check_read(
&mut self,
path: &Path,
diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs
index 1b00dc2c7..1ac8779af 100644
--- a/runtime/permissions/lib.rs
+++ b/runtime/permissions/lib.rs
@@ -21,6 +21,7 @@ use fqdn::FQDN;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::collections::HashSet;
+use std::ffi::OsStr;
use std::fmt;
use std::fmt::Debug;
use std::hash::Hash;
@@ -1642,6 +1643,91 @@ impl PermissionsContainer {
}
#[inline(always)]
+ pub fn check_sys_all(&mut self) -> Result<(), AnyError> {
+ self.0.lock().sys.check_all()
+ }
+
+ #[inline(always)]
+ pub fn check_ffi_all(&mut self) -> Result<(), AnyError> {
+ self.0.lock().ffi.check_all()
+ }
+
+ /// This checks to see if the allow-all flag was passed, not whether all
+ /// permissions are enabled!
+ #[inline(always)]
+ pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> {
+ self.0.lock().all.check()
+ }
+
+ /// Checks special file access, returning the failed permission type if
+ /// not successful.
+ pub fn check_special_file(
+ &mut self,
+ path: &Path,
+ _api_name: &str,
+ ) -> Result<(), &'static str> {
+ let error_all = |_| "all";
+
+ // Safe files with no major additional side-effects. While there's a small risk of someone
+ // draining system entropy by just reading one of these files constantly, that's not really
+ // something we worry about as they already have --allow-read to /dev.
+ if cfg!(unix)
+ && (path == OsStr::new("/dev/random")
+ || path == OsStr::new("/dev/urandom")
+ || path == OsStr::new("/dev/zero")
+ || path == OsStr::new("/dev/null"))
+ {
+ return Ok(());
+ }
+
+ if cfg!(target_os = "linux") {
+ if path.starts_with("/dev")
+ || path.starts_with("/proc")
+ || path.starts_with("/sys")
+ {
+ if path.ends_with("/environ") {
+ self.check_env_all().map_err(|_| "env")?;
+ } else {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ }
+ if path.starts_with("/etc") {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ } else if cfg!(unix) {
+ if path.starts_with("/dev") {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ if path.starts_with("/etc") {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ if path.starts_with("/private/etc") {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ } else if cfg!(target_os = "windows") {
+ fn is_normalized_windows_drive_path(path: &Path) -> bool {
+ let s = path.as_os_str().as_encoded_bytes();
+ // \\?\X:\
+ if s.len() < 7 {
+ false
+ } else if s.starts_with(br#"\\?\"#) {
+ s[4].is_ascii_alphabetic() && s[5] == b':' && s[6] == b'\\'
+ } else {
+ false
+ }
+ }
+
+ // If this is a normalized drive path, accept it
+ if !is_normalized_windows_drive_path(path) {
+ self.check_was_allow_all_flag_passed().map_err(error_all)?;
+ }
+ } else {
+ unimplemented!()
+ }
+ Ok(())
+ }
+
+ #[inline(always)]
pub fn check_net_url(
&mut self,
url: &Url,
@@ -2795,7 +2881,6 @@ mod tests {
fn test_check_fail() {
set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::none_with_prompt();
-
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
prompt_value.set(false);
diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs
index 54652e1f1..d5122af84 100644
--- a/runtime/snapshot.rs
+++ b/runtime/snapshot.rs
@@ -10,6 +10,7 @@ use deno_core::snapshot::*;
use deno_core::v8;
use deno_core::Extension;
use deno_http::DefaultHttpPropertyExtractor;
+use deno_io::fs::FsError;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
@@ -129,6 +130,17 @@ impl deno_net::NetPermissions for Permissions {
}
impl deno_fs::FsPermissions for Permissions {
+ fn check_open<'a>(
+ &mut self,
+ _resolved: bool,
+ _read: bool,
+ _write: bool,
+ _path: &'a Path,
+ _api_name: &str,
+ ) -> Result<std::borrow::Cow<'a, Path>, FsError> {
+ unreachable!("snapshotting!")
+ }
+
fn check_read(
&mut self,
_path: &Path,