1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

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.
This commit is contained in:
Matt Mastracci 2024-04-19 18:12:03 -06:00 committed by GitHub
parent 365e1f48f7
commit 472a370640
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 476 additions and 121 deletions

View file

@ -112,7 +112,7 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer {
Some(source) => source, Some(source) => source,
None => self None => self
.fs .fs
.read_text_file_sync(&specifier.to_file_path().unwrap())?, .read_text_file_sync(&specifier.to_file_path().unwrap(), None)?,
}; };
let analysis = self.inner_cjs_analysis(specifier, &source)?; let analysis = self.inner_cjs_analysis(specifier, &source)?;
match analysis { match analysis {

View file

@ -305,7 +305,7 @@ impl NpmModuleLoader {
let file_path = specifier.to_file_path().unwrap(); let file_path = specifier.to_file_path().unwrap();
let code = self let code = self
.fs .fs
.read_text_file_sync(&file_path) .read_text_file_sync(&file_path, None)
.map_err(AnyError::from) .map_err(AnyError::from)
.with_context(|| { .with_context(|| {
if file_path.is_dir() { if file_path.is_dir() {

View file

@ -5,6 +5,7 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use deno_runtime::deno_fs::AccessCheckCb;
use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_fs::FileSystem;
use deno_runtime::deno_fs::FsDirEntry; use deno_runtime::deno_fs::FsDirEntry;
use deno_runtime::deno_fs::FsFileType; use deno_runtime::deno_fs::FsFileType;
@ -47,6 +48,7 @@ impl DenoCompileFileSystem {
create_new: false, create_new: false,
mode: None, mode: None,
}, },
None,
&old_file_bytes, &old_file_bytes,
) )
} }
@ -75,22 +77,24 @@ impl FileSystem for DenoCompileFileSystem {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
if self.0.is_path_within(path) { if self.0.is_path_within(path) {
Ok(self.0.open_file(path)?) Ok(self.0.open_file(path)?)
} else { } else {
RealFs.open_sync(path, options) RealFs.open_sync(path, options, access_check)
} }
} }
async fn open_async( async fn open_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
if self.0.is_path_within(&path) { if self.0.is_path_within(&path) {
Ok(self.0.open_file(&path)?) Ok(self.0.open_file(&path)?)
} else { } else {
RealFs.open_async(path, options).await RealFs.open_async(path, options, access_check).await
} }
} }

View file

@ -105,7 +105,7 @@ impl GitIgnoreTree {
}); });
let current = self let current = self
.fs .fs
.read_text_file_sync(&dir_path.join(".gitignore")) .read_text_file_sync(&dir_path.join(".gitignore"), None)
.ok() .ok()
.and_then(|text| { .and_then(|text| {
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path); let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path);

View file

@ -19,6 +19,7 @@ use deno_io::fs::FsError;
use deno_io::fs::FsResult; use deno_io::fs::FsResult;
use deno_io::fs::FsStat; use deno_io::fs::FsStat;
use crate::interface::AccessCheckCb;
use crate::interface::FsDirEntry; use crate::interface::FsDirEntry;
use crate::interface::FsFileType; use crate::interface::FsFileType;
use crate::FileSystem; use crate::FileSystem;
@ -48,6 +49,7 @@ impl InMemoryFs {
.write_file_sync( .write_file_sync(
&path, &path,
OpenOptions::write(true, false, false, None), OpenOptions::write(true, false, false, None),
None,
&text.into_bytes(), &text.into_bytes(),
) )
.unwrap(); .unwrap();
@ -82,15 +84,17 @@ impl FileSystem for InMemoryFs {
&self, &self,
_path: &Path, _path: &Path,
_options: OpenOptions, _options: OpenOptions,
_access_check: Option<AccessCheckCb>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
Err(FsError::NotSupported) Err(FsError::NotSupported)
} }
async fn open_async( async fn open_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
self.open_sync(&path, options) self.open_sync(&path, options, access_check)
} }
fn mkdir_sync( fn mkdir_sync(
@ -350,6 +354,7 @@ impl FileSystem for InMemoryFs {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
_access_check: Option<AccessCheckCb>,
data: &[u8], data: &[u8],
) -> FsResult<()> { ) -> FsResult<()> {
let path = normalize_path(path); let path = normalize_path(path);
@ -397,16 +402,21 @@ impl FileSystem for InMemoryFs {
} }
} }
async fn write_file_async( async fn write_file_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
data: Vec<u8>, data: Vec<u8>,
) -> FsResult<()> { ) -> FsResult<()> {
self.write_file_sync(&path, options, &data) self.write_file_sync(&path, options, access_check, &data)
} }
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> { fn read_file_sync(
&self,
path: &Path,
_access_check: Option<AccessCheckCb>,
) -> FsResult<Vec<u8>> {
let entry = self.get_entry(path); let entry = self.get_entry(path);
match entry { match entry {
Some(entry) => match &*entry { Some(entry) => match &*entry {
@ -419,7 +429,11 @@ impl FileSystem for InMemoryFs {
None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))), None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))),
} }
} }
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> { async fn read_file_async<'a>(
self.read_file_sync(&path) &'a self,
path: PathBuf,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Vec<u8>> {
self.read_file_sync(&path, access_check)
} }
} }

View file

@ -80,6 +80,25 @@ pub struct FsDirEntry {
#[allow(clippy::disallowed_types)] #[allow(clippy::disallowed_types)]
pub type FileSystemRc = crate::sync::MaybeArc<dyn FileSystem>; pub type FileSystemRc = crate::sync::MaybeArc<dyn FileSystem>;
pub trait AccessCheckFn:
for<'a> FnMut(
bool,
&'a Path,
&'a OpenOptions,
) -> FsResult<std::borrow::Cow<'a, Path>>
{
}
impl<T> AccessCheckFn for T where
T: for<'a> FnMut(
bool,
&'a Path,
&'a OpenOptions,
) -> FsResult<std::borrow::Cow<'a, Path>>
{
}
pub type AccessCheckCb<'a> = &'a mut (dyn AccessCheckFn + 'a);
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync { pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
fn cwd(&self) -> FsResult<PathBuf>; fn cwd(&self) -> FsResult<PathBuf>;
@ -91,11 +110,13 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb>,
) -> FsResult<Rc<dyn File>>; ) -> FsResult<Rc<dyn File>>;
async fn open_async( async fn open_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Rc<dyn File>>; ) -> FsResult<Rc<dyn File>>;
fn mkdir_sync(&self, path: &Path, recursive: bool, mode: u32) fn mkdir_sync(&self, path: &Path, recursive: bool, mode: u32)
@ -202,22 +223,24 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb>,
data: &[u8], data: &[u8],
) -> FsResult<()> { ) -> FsResult<()> {
let file = self.open_sync(path, options)?; let file = self.open_sync(path, options, access_check)?;
if let Some(mode) = options.mode { if let Some(mode) = options.mode {
file.clone().chmod_sync(mode)?; file.clone().chmod_sync(mode)?;
} }
file.write_all_sync(data)?; file.write_all_sync(data)?;
Ok(()) Ok(())
} }
async fn write_file_async( async fn write_file_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
data: Vec<u8>, data: Vec<u8>,
) -> FsResult<()> { ) -> FsResult<()> {
let file = self.open_async(path, options).await?; let file = self.open_async(path, options, access_check).await?;
if let Some(mode) = options.mode { if let Some(mode) = options.mode {
file.clone().chmod_async(mode).await?; file.clone().chmod_async(mode).await?;
} }
@ -225,15 +248,23 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
Ok(()) Ok(())
} }
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> { fn read_file_sync(
&self,
path: &Path,
access_check: Option<AccessCheckCb>,
) -> FsResult<Vec<u8>> {
let options = OpenOptions::read(); let options = OpenOptions::read();
let file = self.open_sync(path, options)?; let file = self.open_sync(path, options, access_check)?;
let buf = file.read_all_sync()?; let buf = file.read_all_sync()?;
Ok(buf) Ok(buf)
} }
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> { async fn read_file_async<'a>(
&'a self,
path: PathBuf,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Vec<u8>> {
let options = OpenOptions::read(); let options = OpenOptions::read();
let file = self.open_async(path, options).await?; let file = self.open_async(path, options, access_check).await?;
let buf = file.read_all_async().await?; let buf = file.read_all_async().await?;
Ok(buf) Ok(buf)
} }
@ -253,14 +284,22 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
self.stat_sync(path).is_ok() self.stat_sync(path).is_ok()
} }
fn read_text_file_sync(&self, path: &Path) -> FsResult<String> { fn read_text_file_sync(
let buf = self.read_file_sync(path)?; &self,
path: &Path,
access_check: Option<AccessCheckCb>,
) -> FsResult<String> {
let buf = self.read_file_sync(path, access_check)?;
String::from_utf8(buf).map_err(|err| { String::from_utf8(buf).map_err(|err| {
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into() std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
}) })
} }
async fn read_text_file_async(&self, path: PathBuf) -> FsResult<String> { async fn read_text_file_async<'a>(
let buf = self.read_file_async(path).await?; &'a self,
path: PathBuf,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<String> {
let buf = self.read_file_async(path, access_check).await?;
String::from_utf8(buf).map_err(|err| { String::from_utf8(buf).map_err(|err| {
std::io::Error::new(std::io::ErrorKind::InvalidData, err).into() std::io::Error::new(std::io::ErrorKind::InvalidData, err).into()
}) })

View file

@ -7,6 +7,8 @@ mod std_fs;
pub mod sync; pub mod sync;
pub use crate::in_memory_fs::InMemoryFs; pub use crate::in_memory_fs::InMemoryFs;
pub use crate::interface::AccessCheckCb;
pub use crate::interface::AccessCheckFn;
pub use crate::interface::FileSystem; pub use crate::interface::FileSystem;
pub use crate::interface::FileSystemRc; pub use crate::interface::FileSystemRc;
pub use crate::interface::FsDirEntry; pub use crate::interface::FsDirEntry;
@ -20,9 +22,18 @@ use crate::ops::*;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::OpState; use deno_core::OpState;
use deno_io::fs::FsError;
use std::path::Path; use std::path::Path;
pub trait FsPermissions { pub trait FsPermissions: Send + Sync {
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>;
fn check_read(&mut self, path: &Path, api_name: &str) fn check_read(&mut self, path: &Path, api_name: &str)
-> Result<(), AnyError>; -> Result<(), AnyError>;
fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>;
@ -50,19 +61,20 @@ pub trait FsPermissions {
api_name: &str, api_name: &str,
) -> Result<(), AnyError>; ) -> Result<(), AnyError>;
fn check( fn check<'a>(
&mut self, &mut self,
resolved: bool,
open_options: &OpenOptions, open_options: &OpenOptions,
path: &Path, path: &'a Path,
api_name: &str, api_name: &str,
) -> Result<(), AnyError> { ) -> Result<std::borrow::Cow<'a, Path>, FsError> {
if open_options.read { self.check_open(
self.check_read(path, api_name)?; resolved,
} open_options.read,
if open_options.write || open_options.append { open_options.write || open_options.append,
self.check_write(path, api_name)?; path,
} api_name,
Ok(()) )
} }
} }

View file

@ -28,12 +28,55 @@ use rand::Rng;
use serde::Serialize; use serde::Serialize;
use crate::check_unstable; use crate::check_unstable;
use crate::interface::AccessCheckFn;
use crate::interface::FileSystemRc; use crate::interface::FileSystemRc;
use crate::interface::FsDirEntry; use crate::interface::FsDirEntry;
use crate::interface::FsFileType; use crate::interface::FsFileType;
use crate::FsPermissions; use crate::FsPermissions;
use crate::OpenOptions; use crate::OpenOptions;
fn sync_permission_check<'a, P: FsPermissions + 'static>(
permissions: &'a mut P,
api_name: &'static str,
) -> impl AccessCheckFn + 'a {
move |resolved, path, options| {
permissions.check(resolved, options, path, api_name)
}
}
fn async_permission_check<P: FsPermissions + 'static>(
state: Rc<RefCell<OpState>>,
api_name: &'static str,
) -> impl AccessCheckFn {
move |resolved, path, options| {
let mut state = state.borrow_mut();
let permissions = state.borrow_mut::<P>();
permissions.check(resolved, options, path, api_name)
}
}
fn map_permission_error(
operation: &'static str,
error: FsError,
path: &Path,
) -> AnyError {
match error {
FsError::PermissionDenied(err) => {
let path = format!("{path:?}");
let (path, truncated) = if path.len() > 1024 {
(&path[0..1024], "...(truncated)")
} else {
(path.as_str(), "")
};
custom_error("PermissionDenied", format!("Requires {err} access to {path}{truncated}, run again with the --allow-{err} flag"))
}
err => Err::<(), _>(err)
.context_path(operation, path)
.err()
.unwrap(),
}
}
#[op2] #[op2]
#[string] #[string]
pub fn op_fs_cwd<P>(state: &mut OpState) -> Result<String, AnyError> pub fn op_fs_cwd<P>(state: &mut OpState) -> Result<String, AnyError>
@ -89,12 +132,14 @@ where
let path = PathBuf::from(path); let path = PathBuf::from(path);
let options = options.unwrap_or_else(OpenOptions::read); let options = options.unwrap_or_else(OpenOptions::read);
let permissions = state.borrow_mut::<P>();
permissions.check(&options, &path, "Deno.openSync()")?;
let fs = state.borrow::<FileSystemRc>();
let file = fs.open_sync(&path, options).context_path("open", &path)?;
let fs = state.borrow::<FileSystemRc>().clone();
let mut access_check =
sync_permission_check::<P>(state.borrow_mut(), "Deno.openSync()");
let file = fs
.open_sync(&path, options, Some(&mut access_check))
.map_err(|error| map_permission_error("open", error, &path))?;
drop(access_check);
let rid = state let rid = state
.resource_table .resource_table
.add(FileResource::new(file, "fsFile".to_string())); .add(FileResource::new(file, "fsFile".to_string()));
@ -114,16 +159,13 @@ where
let path = PathBuf::from(path); let path = PathBuf::from(path);
let options = options.unwrap_or_else(OpenOptions::read); let options = options.unwrap_or_else(OpenOptions::read);
let fs = { let mut access_check =
let mut state = state.borrow_mut(); async_permission_check::<P>(state.clone(), "Deno.open()");
let permissions = state.borrow_mut::<P>(); let fs = state.borrow().borrow::<FileSystemRc>().clone();
permissions.check(&options, &path, "Deno.open()")?;
state.borrow::<FileSystemRc>().clone()
};
let file = fs let file = fs
.open_async(path.clone(), options) .open_async(path.clone(), options, Some(&mut access_check))
.await .await
.context_path("open", &path)?; .map_err(|error| map_permission_error("open", error, &path))?;
let rid = state let rid = state
.borrow_mut() .borrow_mut()
@ -961,11 +1003,10 @@ where
}; };
let mut rng = thread_rng(); let mut rng = thread_rng();
const MAX_TRIES: u32 = 10; const MAX_TRIES: u32 = 10;
for _ in 0..MAX_TRIES { for _ in 0..MAX_TRIES {
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
match fs.open_sync(&path, open_opts) { match fs.open_sync(&path, open_opts, None) {
Ok(_) => return path_into_string(path.into_os_string()), Ok(_) => return path_into_string(path.into_os_string()),
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
continue; continue;
@ -1007,7 +1048,7 @@ where
const MAX_TRIES: u32 = 10; const MAX_TRIES: u32 = 10;
for _ in 0..MAX_TRIES { for _ in 0..MAX_TRIES {
let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
match fs.clone().open_async(path.clone(), open_opts).await { match fs.clone().open_async(path.clone(), open_opts, None).await {
Ok(_) => return path_into_string(path.into_os_string()), Ok(_) => return path_into_string(path.into_os_string()),
Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
continue; continue;
@ -1150,14 +1191,13 @@ where
{ {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let permissions = state.borrow_mut::<P>();
let options = OpenOptions::write(create, append, create_new, mode); let options = OpenOptions::write(create, append, create_new, mode);
permissions.check(&options, &path, "Deno.writeFileSync()")?; let fs = state.borrow::<FileSystemRc>().clone();
let mut access_check =
sync_permission_check::<P>(state.borrow_mut(), "Deno.writeFileSync()");
let fs = state.borrow::<FileSystemRc>(); fs.write_file_sync(&path, options, Some(&mut access_check), &data)
.map_err(|error| map_permission_error("writefile", error, &path))?;
fs.write_file_sync(&path, options, &data)
.context_path("writefile", &path)?;
Ok(()) Ok(())
} }
@ -1181,16 +1221,21 @@ where
let options = OpenOptions::write(create, append, create_new, mode); let options = OpenOptions::write(create, append, create_new, mode);
let mut access_check =
async_permission_check::<P>(state.clone(), "Deno.writeFile()");
let (fs, cancel_handle) = { let (fs, cancel_handle) = {
let mut state = state.borrow_mut(); let state = state.borrow_mut();
let permissions = state.borrow_mut::<P>();
permissions.check(&options, &path, "Deno.writeFile()")?;
let cancel_handle = cancel_rid let cancel_handle = cancel_rid
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok()); .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
(state.borrow::<FileSystemRc>().clone(), cancel_handle) (state.borrow::<FileSystemRc>().clone(), cancel_handle)
}; };
let fut = fs.write_file_async(path.clone(), options, data.to_vec()); let fut = fs.write_file_async(
path.clone(),
options,
Some(&mut access_check),
data.to_vec(),
);
if let Some(cancel_handle) = cancel_handle { if let Some(cancel_handle) = cancel_handle {
let res = fut.or_cancel(cancel_handle).await; let res = fut.or_cancel(cancel_handle).await;
@ -1201,9 +1246,11 @@ where
} }
}; };
res?.context_path("writefile", &path)?; res?.map_err(|error| map_permission_error("writefile", error, &path))?;
} else { } else {
fut.await.context_path("writefile", &path)?; fut
.await
.map_err(|error| map_permission_error("writefile", error, &path))?;
} }
Ok(()) Ok(())
@ -1220,11 +1267,12 @@ where
{ {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let permissions = state.borrow_mut::<P>(); let fs = state.borrow::<FileSystemRc>().clone();
permissions.check_read(&path, "Deno.readFileSync()")?; let mut access_check =
sync_permission_check::<P>(state.borrow_mut(), "Deno.readFileSync()");
let fs = state.borrow::<FileSystemRc>(); let buf = fs
let buf = fs.read_file_sync(&path).context_path("readfile", &path)?; .read_file_sync(&path, Some(&mut access_check))
.map_err(|error| map_permission_error("readfile", error, &path))?;
Ok(buf.into()) Ok(buf.into())
} }
@ -1241,16 +1289,16 @@ where
{ {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let mut access_check =
async_permission_check::<P>(state.clone(), "Deno.readFile()");
let (fs, cancel_handle) = { let (fs, cancel_handle) = {
let mut state = state.borrow_mut(); let state = state.borrow();
let permissions = state.borrow_mut::<P>();
permissions.check_read(&path, "Deno.readFile()")?;
let cancel_handle = cancel_rid let cancel_handle = cancel_rid
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok()); .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
(state.borrow::<FileSystemRc>().clone(), cancel_handle) (state.borrow::<FileSystemRc>().clone(), cancel_handle)
}; };
let fut = fs.read_file_async(path.clone()); let fut = fs.read_file_async(path.clone(), Some(&mut access_check));
let buf = if let Some(cancel_handle) = cancel_handle { let buf = if let Some(cancel_handle) = cancel_handle {
let res = fut.or_cancel(cancel_handle).await; let res = fut.or_cancel(cancel_handle).await;
@ -1261,9 +1309,11 @@ where
} }
}; };
res?.context_path("readfile", &path)? res?.map_err(|error| map_permission_error("readfile", error, &path))?
} else { } else {
fut.await.context_path("readfile", &path)? fut
.await
.map_err(|error| map_permission_error("readfile", error, &path))?
}; };
Ok(buf.into()) Ok(buf.into())
@ -1280,11 +1330,12 @@ where
{ {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let permissions = state.borrow_mut::<P>(); let fs = state.borrow::<FileSystemRc>().clone();
permissions.check_read(&path, "Deno.readFileSync()")?; let mut access_check =
sync_permission_check::<P>(state.borrow_mut(), "Deno.readFileSync()");
let fs = state.borrow::<FileSystemRc>(); let buf = fs
let buf = fs.read_file_sync(&path).context_path("readfile", &path)?; .read_file_sync(&path, Some(&mut access_check))
.map_err(|error| map_permission_error("readfile", error, &path))?;
Ok(string_from_utf8_lossy(buf)) Ok(string_from_utf8_lossy(buf))
} }
@ -1301,16 +1352,16 @@ where
{ {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let mut access_check =
async_permission_check::<P>(state.clone(), "Deno.readFile()");
let (fs, cancel_handle) = { let (fs, cancel_handle) = {
let mut state = state.borrow_mut(); let state = state.borrow_mut();
let permissions = state.borrow_mut::<P>();
permissions.check_read(&path, "Deno.readFile()")?;
let cancel_handle = cancel_rid let cancel_handle = cancel_rid
.and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok()); .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
(state.borrow::<FileSystemRc>().clone(), cancel_handle) (state.borrow::<FileSystemRc>().clone(), cancel_handle)
}; };
let fut = fs.read_file_async(path.clone()); let fut = fs.read_file_async(path.clone(), Some(&mut access_check));
let buf = if let Some(cancel_handle) = cancel_handle { let buf = if let Some(cancel_handle) = cancel_handle {
let res = fut.or_cancel(cancel_handle).await; let res = fut.or_cancel(cancel_handle).await;
@ -1321,9 +1372,11 @@ where
} }
}; };
res?.context_path("readfile", &path)? res?.map_err(|error| map_permission_error("readfile", error, &path))?
} else { } else {
fut.await.context_path("readfile", &path)? fut
.await
.map_err(|error| map_permission_error("readfile", error, &path))?
}; };
Ok(string_from_utf8_lossy(buf)) Ok(string_from_utf8_lossy(buf))

View file

@ -2,27 +2,29 @@
#![allow(clippy::disallowed_methods)] #![allow(clippy::disallowed_methods)]
use std::env::current_dir;
use std::fs; use std::fs;
use std::io; use std::io;
use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use deno_core::normalize_path;
use deno_core::unsync::spawn_blocking; use deno_core::unsync::spawn_blocking;
use deno_io::fs::File; use deno_io::fs::File;
use deno_io::fs::FsError;
use deno_io::fs::FsResult; use deno_io::fs::FsResult;
use deno_io::fs::FsStat; use deno_io::fs::FsStat;
use deno_io::StdFileResourceInner; use deno_io::StdFileResourceInner;
use crate::interface::AccessCheckCb;
use crate::interface::FsDirEntry; use crate::interface::FsDirEntry;
use crate::interface::FsFileType; use crate::interface::FsFileType;
use crate::FileSystem; use crate::FileSystem;
use crate::OpenOptions; use crate::OpenOptions;
#[cfg(not(unix))]
use deno_io::fs::FsError;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RealFs; pub struct RealFs;
@ -80,18 +82,18 @@ impl FileSystem for RealFs {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
let opts = open_options(options); let std_file = open_with_access_check(options, path, access_check)?;
let std_file = opts.open(path)?;
Ok(Rc::new(StdFileResourceInner::file(std_file))) Ok(Rc::new(StdFileResourceInner::file(std_file)))
} }
async fn open_async( async fn open_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Rc<dyn File>> { ) -> FsResult<Rc<dyn File>> {
let opts = open_options(options); let std_file = open_with_access_check(options, &path, access_check)?;
let std_file = spawn_blocking(move || opts.open(path)).await??;
Ok(Rc::new(StdFileResourceInner::file(std_file))) Ok(Rc::new(StdFileResourceInner::file(std_file)))
} }
@ -276,10 +278,10 @@ impl FileSystem for RealFs {
&self, &self,
path: &Path, path: &Path,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb>,
data: &[u8], data: &[u8],
) -> FsResult<()> { ) -> FsResult<()> {
let opts = open_options(options); let mut file = open_with_access_check(options, path, access_check)?;
let mut file = opts.open(path)?;
#[cfg(unix)] #[cfg(unix)]
if let Some(mode) = options.mode { if let Some(mode) = options.mode {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
@ -289,15 +291,15 @@ impl FileSystem for RealFs {
Ok(()) Ok(())
} }
async fn write_file_async( async fn write_file_async<'a>(
&self, &'a self,
path: PathBuf, path: PathBuf,
options: OpenOptions, options: OpenOptions,
access_check: Option<AccessCheckCb<'a>>,
data: Vec<u8>, data: Vec<u8>,
) -> FsResult<()> { ) -> FsResult<()> {
let mut file = open_with_access_check(options, &path, access_check)?;
spawn_blocking(move || { spawn_blocking(move || {
let opts = open_options(options);
let mut file = opts.open(path)?;
#[cfg(unix)] #[cfg(unix)]
if let Some(mode) = options.mode { if let Some(mode) = options.mode {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
@ -309,11 +311,41 @@ impl FileSystem for RealFs {
.await? .await?
} }
fn read_file_sync(&self, path: &Path) -> FsResult<Vec<u8>> { fn read_file_sync(
fs::read(path).map_err(Into::into) &self,
path: &Path,
access_check: Option<AccessCheckCb>,
) -> FsResult<Vec<u8>> {
let mut file = open_with_access_check(
OpenOptions {
read: true,
..Default::default()
},
path,
access_check,
)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
Ok(buf)
} }
async fn read_file_async(&self, path: PathBuf) -> FsResult<Vec<u8>> { async fn read_file_async<'a>(
spawn_blocking(move || fs::read(path)) &'a self,
path: PathBuf,
access_check: Option<AccessCheckCb<'a>>,
) -> FsResult<Vec<u8>> {
let mut file = open_with_access_check(
OpenOptions {
read: true,
..Default::default()
},
&path,
access_check,
)?;
spawn_blocking(move || {
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
Ok::<_, FsError>(buf)
})
.await? .await?
.map_err(Into::into) .map_err(Into::into)
} }
@ -410,7 +442,6 @@ fn copy_file(from: &Path, to: &Path) -> FsResult<()> {
use libc::stat; use libc::stat;
use libc::unlink; use libc::unlink;
use std::ffi::CString; use std::ffi::CString;
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
@ -845,3 +876,60 @@ fn open_options(options: OpenOptions) -> fs::OpenOptions {
open_options.create_new(options.create_new); open_options.create_new(options.create_new);
open_options open_options
} }
#[inline(always)]
fn open_with_access_check(
options: OpenOptions,
path: &Path,
access_check: Option<AccessCheckCb>,
) -> FsResult<std::fs::File> {
if let Some(access_check) = access_check {
let path = if path.is_absolute() {
normalize_path(path)
} else {
let cwd = current_dir()?;
normalize_path(cwd.join(path))
};
(*access_check)(false, &path, &options)?;
// On Linux, /proc may contain magic links that we don't want to resolve
let needs_canonicalization =
!cfg!(target_os = "linux") || path.starts_with("/proc");
let path = if needs_canonicalization {
match path.canonicalize() {
Ok(path) => path,
Err(_) => {
if let (Some(parent), Some(filename)) =
(path.parent(), path.file_name())
{
parent.canonicalize()?.join(filename)
} else {
return Err(std::io::ErrorKind::NotFound.into());
}
}
}
} else {
path
};
(*access_check)(true, &path, &options)?;
// For windows
#[allow(unused_mut)]
let mut opts: fs::OpenOptions = open_options(options);
#[cfg(unix)]
{
// Don't follow symlinks on open -- we must always pass fully-resolved files
// with the exception of /proc/ which is too special, and /dev/std* which might point to
// proc.
use std::os::unix::fs::OpenOptionsExt;
if needs_canonicalization {
opts.custom_flags(libc::O_NOFOLLOW);
}
}
Ok(opts.open(&path)?)
} else {
let opts = open_options(options);
Ok(opts.open(path)?)
}
}

View file

@ -6,6 +6,7 @@ use std::rc::Rc;
use std::time::SystemTime; use std::time::SystemTime;
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use deno_core::error::custom_error;
use deno_core::error::not_supported; use deno_core::error::not_supported;
use deno_core::error::resource_unavailable; use deno_core::error::resource_unavailable;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -21,6 +22,7 @@ pub enum FsError {
Io(io::Error), Io(io::Error),
FileBusy, FileBusy,
NotSupported, NotSupported,
PermissionDenied(&'static str),
} }
impl FsError { impl FsError {
@ -29,6 +31,7 @@ impl FsError {
Self::Io(err) => err.kind(), Self::Io(err) => err.kind(),
Self::FileBusy => io::ErrorKind::Other, Self::FileBusy => io::ErrorKind::Other,
Self::NotSupported => io::ErrorKind::Other, Self::NotSupported => io::ErrorKind::Other,
Self::PermissionDenied(_) => io::ErrorKind::PermissionDenied,
} }
} }
@ -37,6 +40,9 @@ impl FsError {
FsError::Io(err) => err, FsError::Io(err) => err,
FsError::FileBusy => io::Error::new(self.kind(), "file busy"), FsError::FileBusy => io::Error::new(self.kind(), "file busy"),
FsError::NotSupported => io::Error::new(self.kind(), "not supported"), FsError::NotSupported => io::Error::new(self.kind(), "not supported"),
FsError::PermissionDenied(err) => {
io::Error::new(self.kind(), format!("requires {err} access"))
}
} }
} }
} }
@ -47,12 +53,21 @@ impl From<io::Error> for FsError {
} }
} }
impl From<io::ErrorKind> for FsError {
fn from(err: io::ErrorKind) -> Self {
Self::Io(err.into())
}
}
impl From<FsError> for AnyError { impl From<FsError> for AnyError {
fn from(err: FsError) -> Self { fn from(err: FsError) -> Self {
match err { match err {
FsError::Io(err) => AnyError::from(err), FsError::Io(err) => AnyError::from(err),
FsError::FileBusy => resource_unavailable(), FsError::FileBusy => resource_unavailable(),
FsError::NotSupported => not_supported(), FsError::NotSupported => not_supported(),
FsError::PermissionDenied(err) => {
custom_error("PermissionDenied", format!("permission denied: {err}"))
}
} }
} }
} }

View file

@ -452,7 +452,7 @@ where
let file_path = PathBuf::from(file_path); let file_path = PathBuf::from(file_path);
ensure_read_permission::<P>(state, &file_path)?; ensure_read_permission::<P>(state, &file_path)?;
let fs = state.borrow::<FileSystemRc>(); let fs = state.borrow::<FileSystemRc>();
Ok(fs.read_text_file_sync(&file_path)?) Ok(fs.read_text_file_sync(&file_path, None)?)
} }
#[op2] #[op2]

View file

@ -82,7 +82,7 @@ impl PackageJson {
return Ok(CACHE.with(|cache| cache.borrow()[&path].clone())); return Ok(CACHE.with(|cache| cache.borrow()[&path].clone()));
} }
let source = match fs.read_text_file_sync(&path) { let source = match fs.read_text_file_sync(&path, None) {
Ok(source) => source, Ok(source) => source,
Err(err) if err.kind() == ErrorKind::NotFound => { Err(err) if err.kind() == ErrorKind::NotFound => {
return Ok(Rc::new(PackageJson::empty(path))); return Ok(Rc::new(PackageJson::empty(path)));

View file

@ -1,9 +1,11 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::borrow::Cow;
use std::path::Path; use std::path::Path;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::url::Url; use deno_core::url::Url;
pub use deno_io::fs::FsError;
pub use deno_permissions::create_child_permissions; pub use deno_permissions::create_child_permissions;
pub use deno_permissions::parse_sys_kind; pub use deno_permissions::parse_sys_kind;
pub use deno_permissions::set_prompt_callbacks; pub use deno_permissions::set_prompt_callbacks;
@ -142,6 +144,34 @@ impl deno_websocket::WebSocketPermissions for PermissionsContainer {
} }
impl deno_fs::FsPermissions 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( fn check_read(
&mut self, &mut self,
path: &Path, path: &Path,

View file

@ -21,6 +21,7 @@ use fqdn::FQDN;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::OsStr;
use std::fmt; use std::fmt;
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
@ -1641,6 +1642,91 @@ impl PermissionsContainer {
self.0.lock().env.check_all() self.0.lock().env.check_all()
} }
#[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)] #[inline(always)]
pub fn check_net_url( pub fn check_net_url(
&mut self, &mut self,
@ -2795,7 +2881,6 @@ mod tests {
fn test_check_fail() { fn test_check_fail() {
set_prompter(Box::new(TestPrompter)); set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::none_with_prompt(); let mut perms = Permissions::none_with_prompt();
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
prompt_value.set(false); prompt_value.set(false);

View file

@ -10,6 +10,7 @@ use deno_core::snapshot::*;
use deno_core::v8; use deno_core::v8;
use deno_core::Extension; use deno_core::Extension;
use deno_http::DefaultHttpPropertyExtractor; use deno_http::DefaultHttpPropertyExtractor;
use deno_io::fs::FsError;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -129,6 +130,17 @@ impl deno_net::NetPermissions for Permissions {
} }
impl deno_fs::FsPermissions 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( fn check_read(
&mut self, &mut self,
_path: &Path, _path: &Path,

View file

@ -704,7 +704,7 @@ fn permission_request_long() {
.args_vec(["run", "--quiet", "run/permission_request_long.ts"]) .args_vec(["run", "--quiet", "run/permission_request_long.ts"])
.with_pty(|mut console| { .with_pty(|mut console| {
console.expect(concat!( console.expect(concat!(
"❌ Permission prompt length (100017 bytes) was larger than the configured maximum length (10240 bytes): denying request.\r\n", "was larger than the configured maximum length (10240 bytes): denying request.\r\n",
"❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests.\r\n", "❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests.\r\n",
"❌ Run again with --allow-read to bypass this check if this is really what you want to do.\r\n", "❌ Run again with --allow-read to bypass this check if this is really what you want to do.\r\n",
)); ));
@ -4309,7 +4309,10 @@ fn fsfile_set_raw_should_not_panic_on_no_tty() {
.unwrap(); .unwrap();
assert!(!output.status.success()); assert!(!output.status.success());
let stderr = std::str::from_utf8(&output.stderr).unwrap().trim(); let stderr = std::str::from_utf8(&output.stderr).unwrap().trim();
assert!(stderr.contains("BadResource")); assert!(
stderr.contains("BadResource"),
"stderr did not contain BadResource: {stderr}"
);
} }
#[test] #[test]
@ -4674,7 +4677,7 @@ fn stdio_streams_are_locked_in_permission_prompt() {
console.write_line(r#"new Worker(URL.createObjectURL(new Blob(["setInterval(() => console.log('**malicious**'), 10)"])), { type: "module" });"#); console.write_line(r#"new Worker(URL.createObjectURL(new Blob(["setInterval(() => console.log('**malicious**'), 10)"])), { type: "module" });"#);
// The worker is now spamming // The worker is now spamming
console.expect(malicious_output); console.expect(malicious_output);
console.write_line(r#"Deno.readTextFileSync('Cargo.toml');"#); console.write_line(r#"Deno.readTextFileSync('../Cargo.toml');"#);
// We will get a permission prompt // We will get a permission prompt
console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) > "); console.expect("Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) > ");
// The worker is blocked, so nothing else should get written here // The worker is blocked, so nothing else should get written here
@ -4690,7 +4693,7 @@ fn stdio_streams_are_locked_in_permission_prompt() {
console.human_delay(); console.human_delay();
console.write_line_raw("y"); console.write_line_raw("y");
// We ensure that nothing gets written here between the permission prompt and this text, despire the delay // We ensure that nothing gets written here between the permission prompt and this text, despire the delay
console.expect_raw_next(format!("y{newline}\x1b[4A\x1b[0J✅ Granted read access to \"Cargo.toml\".")); console.expect_raw_next(format!("y{newline}\x1b[4A\x1b[0J✅ Granted read access to \""));
// Back to spamming! // Back to spamming!
console.expect(malicious_output); console.expect(malicious_output);

View file

@ -173,7 +173,7 @@ Deno.test(
await Deno.readFile("tests/testdata/assets/"); await Deno.readFile("tests/testdata/assets/");
} catch (e) { } catch (e) {
if (Deno.build.os === "windows") { if (Deno.build.os === "windows") {
assertEquals(e.code, "ENOENT"); assertEquals(e.code, "EPERM");
} else { } else {
assertEquals(e.code, "EISDIR"); assertEquals(e.code, "EISDIR");
} }