From 32aeec9630dc91162f0408b95dd86e1c26e4c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Mon, 11 May 2020 13:13:27 +0200 Subject: [PATCH] refactor: check permissions in SourceFileFetcher (#5011) This PR hot-fixes permission escapes in dynamic imports, workers and runtime compiler APIs. "permissions" parameter was added to public APIs of SourceFileFetcher and appropriate permission checks are performed during loading of local and remote files. --- cli/file_fetcher.rs | 278 ++++++++++++++++++++++++++++----- cli/global_state.rs | 9 +- cli/lib.rs | 14 +- cli/ops/compiler.rs | 15 +- cli/ops/runtime_compiler.rs | 12 +- cli/permissions.rs | 13 ++ cli/state.rs | 16 +- cli/tests/integration_tests.rs | 2 + cli/tsc.rs | 50 ++++-- docs/runtime/compiler_apis.md | 18 ++- docs/runtime/workers.md | 43 +++++ 11 files changed, 396 insertions(+), 74 deletions(-) diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index c4e0e4fec9c7a3..2e75517e632523 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -6,6 +6,7 @@ use crate::http_util::create_http_client; use crate::http_util::FetchOnceResult; use crate::msg; use crate::op_error::OpError; +use crate::permissions::Permissions; use deno_core::ErrBox; use deno_core::ModuleSpecifier; use futures::future::FutureExt; @@ -109,6 +110,7 @@ impl SourceFileFetcher { pub fn fetch_cached_source_file( &self, specifier: &ModuleSpecifier, + permissions: Permissions, ) -> Option { let maybe_source_file = self.source_file_cache.get(specifier.to_string()); @@ -123,7 +125,7 @@ impl SourceFileFetcher { // future, because it doesn't actually do any asynchronous // action in that path. if let Ok(maybe_source_file) = - self.get_source_file_from_local_cache(specifier.as_url()) + self.get_source_file_from_local_cache(specifier.as_url(), &permissions) { return maybe_source_file; } @@ -148,6 +150,7 @@ impl SourceFileFetcher { &self, specifier: &ModuleSpecifier, maybe_referrer: Option, + permissions: Permissions, ) -> Result { let module_url = specifier.as_url().to_owned(); debug!("fetch_source_file specifier: {} ", &module_url); @@ -167,6 +170,7 @@ impl SourceFileFetcher { self.use_disk_cache, self.no_remote, self.cached_only, + &permissions, ) .await; @@ -222,6 +226,7 @@ impl SourceFileFetcher { fn get_source_file_from_local_cache( &self, module_url: &Url, + permissions: &Permissions, ) -> Result, ErrBox> { let url_scheme = module_url.scheme(); let is_local_file = url_scheme == "file"; @@ -229,7 +234,7 @@ impl SourceFileFetcher { // Local files are always fetched from disk bypassing cache entirely. if is_local_file { - return self.fetch_local_file(&module_url).map(Some); + return self.fetch_local_file(&module_url, permissions).map(Some); } self.fetch_cached_remote_source(&module_url) @@ -252,6 +257,7 @@ impl SourceFileFetcher { use_disk_cache: bool, no_remote: bool, cached_only: bool, + permissions: &Permissions, ) -> Result { let url_scheme = module_url.scheme(); let is_local_file = url_scheme == "file"; @@ -259,7 +265,7 @@ impl SourceFileFetcher { // Local files are always fetched from disk bypassing cache entirely. if is_local_file { - return self.fetch_local_file(&module_url); + return self.fetch_local_file(&module_url, permissions); } // The file is remote, fail if `no_remote` is true. @@ -276,18 +282,29 @@ impl SourceFileFetcher { // Fetch remote file and cache on-disk for subsequent access self - .fetch_remote_source(&module_url, use_disk_cache, cached_only, 10) + .fetch_remote_source( + &module_url, + use_disk_cache, + cached_only, + 10, + permissions, + ) .await } /// Fetch local source file. - fn fetch_local_file(&self, module_url: &Url) -> Result { + fn fetch_local_file( + &self, + module_url: &Url, + permissions: &Permissions, + ) -> Result { let filepath = module_url.to_file_path().map_err(|()| { ErrBox::from(OpError::uri_error( "File URL contains invalid path".to_owned(), )) })?; + permissions.check_read(&filepath)?; let source_code = match fs::read(filepath.clone()) { Ok(c) => c, Err(e) => return Err(e.into()), @@ -390,12 +407,17 @@ impl SourceFileFetcher { use_disk_cache: bool, cached_only: bool, redirect_limit: i64, + permissions: &Permissions, ) -> Pin>>> { if redirect_limit < 0 { let e = OpError::http("too many redirects".to_string()); return futures::future::err(e.into()).boxed_local(); } + if let Err(e) = permissions.check_net_url(&module_url) { + return futures::future::err(e.into()).boxed_local(); + } + let is_blacklisted = check_cache_blacklist(module_url, self.cache_blacklist.as_ref()); // First try local cache @@ -441,6 +463,7 @@ impl SourceFileFetcher { Ok((_, headers)) => headers.get("etag").map(String::from), Err(_) => None, }; + let permissions = permissions.clone(); let http_client = self.http_client.clone(); // Single pass fetch, either yields code or yields redirect. let f = async move { @@ -463,6 +486,7 @@ impl SourceFileFetcher { use_disk_cache, cached_only, redirect_limit - 1, + &permissions, ) .await } @@ -752,11 +776,15 @@ mod tests { if cfg!(windows) { // Should fail: missing drive letter. let u = Url::parse("file:///etc/passwd").unwrap(); - fetcher.fetch_local_file(&u).unwrap_err(); + fetcher + .fetch_local_file(&u, &Permissions::allow_all()) + .unwrap_err(); } else { // Should fail: local network paths are not supported on unix. let u = Url::parse("file://server/etc/passwd").unwrap(); - fetcher.fetch_local_file(&u).unwrap_err(); + fetcher + .fetch_local_file(&u, &Permissions::allow_all()) + .unwrap_err(); } } @@ -774,7 +802,13 @@ mod tests { let cache_filename = fetcher.http_cache.get_cache_filename(&module_url); let result = fetcher - .get_source_file(&module_url, true, false, false) + .get_source_file( + &module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r = result.unwrap(); @@ -795,7 +829,13 @@ mod tests { metadata.write(&cache_filename).unwrap(); let result2 = fetcher_1 - .get_source_file(&module_url, true, false, false) + .get_source_file( + &module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result2.is_ok()); let r2 = result2.unwrap(); @@ -818,7 +858,13 @@ mod tests { metadata.write(&cache_filename).unwrap(); let result3 = fetcher_2 - .get_source_file(&module_url_1, true, false, false) + .get_source_file( + &module_url_1, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result3.is_ok()); let r3 = result3.unwrap(); @@ -839,7 +885,13 @@ mod tests { // and don't use cache let fetcher = setup_file_fetcher(temp_dir.path()); let result4 = fetcher - .get_source_file(&module_url_2, false, false, false) + .get_source_file( + &module_url_2, + false, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result4.is_ok()); let r4 = result4.unwrap(); @@ -863,7 +915,13 @@ mod tests { let cache_filename = fetcher.http_cache.get_cache_filename(&module_url); let result = fetcher - .get_source_file(&module_url, true, false, false) + .get_source_file( + &module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r = result.unwrap(); @@ -883,7 +941,13 @@ mod tests { metadata.write(&cache_filename).unwrap(); let result2 = fetcher - .get_source_file(&module_url, true, false, false) + .get_source_file( + &module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result2.is_ok()); let r2 = result2.unwrap(); @@ -903,7 +967,13 @@ mod tests { // process) and don't use cache let fetcher = setup_file_fetcher(temp_dir.path()); let result3 = fetcher - .get_source_file(&module_url_1, false, false, false) + .get_source_file( + &module_url_1, + false, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result3.is_ok()); let r3 = result3.unwrap(); @@ -930,7 +1000,9 @@ mod tests { fetcher.http_cache.get_cache_filename(&specifier.as_url()); // first download - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_ok()); let headers_file_name = @@ -946,7 +1018,9 @@ mod tests { // `use_disk_cache` is set to false, this can be verified using source // header file creation timestamp (should be the same as after first // download) - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_ok()); let result = fs::File::open(&headers_file_name); @@ -984,7 +1058,13 @@ mod tests { // Test basic follow and headers recording let result = fetcher - .get_source_file(&redirect_module_url, true, false, false) + .get_source_file( + &redirect_module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let mod_meta = result.unwrap(); @@ -1033,7 +1113,13 @@ mod tests { // Test double redirects and headers recording let result = fetcher - .get_source_file(&double_redirect_url, true, false, false) + .get_source_file( + &double_redirect_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let mod_meta = result.unwrap(); @@ -1080,7 +1166,13 @@ mod tests { // Test that redirect target is not downloaded twice for different redirect source. let result = fetcher - .get_source_file(&double_redirect_url, true, false, false) + .get_source_file( + &double_redirect_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let result = fs::File::open(&target_path); @@ -1095,7 +1187,13 @@ mod tests { // using source header file creation timestamp (should be the same as // after first `get_source_file`) let result = fetcher - .get_source_file(&redirect_url, true, false, false) + .get_source_file( + &redirect_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let result = fs::File::open(&target_path_); @@ -1121,12 +1219,24 @@ mod tests { // Test that redirections can be limited let result = fetcher - .fetch_remote_source(&double_redirect_url, false, false, 2) + .fetch_remote_source( + &double_redirect_url, + false, + false, + 2, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let result = fetcher - .fetch_remote_source(&double_redirect_url, false, false, 1) + .fetch_remote_source( + &double_redirect_url, + false, + false, + 1, + &Permissions::allow_all(), + ) .await; assert!(result.is_err()); // FIXME(bartlomieju): @@ -1161,7 +1271,13 @@ mod tests { // Test basic follow and headers recording let result = fetcher - .get_source_file(&redirect_module_url, true, false, false) + .get_source_file( + &redirect_module_url, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let mod_meta = result.unwrap(); @@ -1193,7 +1309,13 @@ mod tests { Url::parse("http://localhost:4545/cli/tests/002_hello.ts").unwrap(); // Remote modules are not allowed let result = fetcher - .get_source_file(&module_url, true, true, false) + .get_source_file( + &module_url, + true, + true, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_err()); // FIXME(bartlomieju): @@ -1216,7 +1338,13 @@ mod tests { // file hasn't been cached before let result = fetcher - .get_source_file(&module_url, true, false, true) + .get_source_file( + &module_url, + true, + false, + true, + &Permissions::allow_all(), + ) .await; assert!(result.is_err()); // FIXME(bartlomieju): @@ -1225,12 +1353,24 @@ mod tests { // download and cache file let result = fetcher_1 - .get_source_file(&module_url_1, true, false, false) + .get_source_file( + &module_url_1, + true, + false, + false, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); // module is already cached, should be ok even with `cached_only` let result = fetcher_2 - .get_source_file(&module_url_2, true, false, true) + .get_source_file( + &module_url_2, + true, + false, + true, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); drop(http_server_guard); @@ -1244,7 +1384,13 @@ mod tests { Url::parse("http://127.0.0.1:4545/cli/tests/subdir/mt_video_mp2t.t3.ts") .unwrap(); let result = fetcher - .fetch_remote_source(&module_url, false, false, 10) + .fetch_remote_source( + &module_url, + false, + false, + 10, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r = result.unwrap(); @@ -1289,7 +1435,13 @@ mod tests { let module_url_3_ = module_url_3.clone(); let result = fetcher - .fetch_remote_source(&module_url, false, false, 10) + .fetch_remote_source( + &module_url, + false, + false, + 10, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r = result.unwrap(); @@ -1298,7 +1450,13 @@ mod tests { let (_, headers) = fetcher.http_cache.get(&module_url).unwrap(); assert_eq!(headers.get("content-type").unwrap(), "text/typescript"); let result = fetcher_1 - .fetch_remote_source(&module_url_2, false, false, 10) + .fetch_remote_source( + &module_url_2, + false, + false, + 10, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r2 = result.unwrap(); @@ -1309,7 +1467,13 @@ mod tests { // test unknown extension let result = fetcher_2 - .fetch_remote_source(&module_url_3, false, false, 10) + .fetch_remote_source( + &module_url_3, + false, + false, + 10, + &Permissions::allow_all(), + ) .await; assert!(result.is_ok()); let r3 = result.unwrap(); @@ -1328,14 +1492,18 @@ mod tests { // Test failure case. let specifier = ModuleSpecifier::resolve_url(file_url!("/baddir/hello.ts")).unwrap(); - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_err()); let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("js/main.ts"); let specifier = ModuleSpecifier::resolve_url_or_path(p.to_str().unwrap()).unwrap(); - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_ok()); } @@ -1347,14 +1515,18 @@ mod tests { // Test failure case. let specifier = ModuleSpecifier::resolve_url(file_url!("/baddir/hello.ts")).unwrap(); - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_err()); let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("js/main.ts"); let specifier = ModuleSpecifier::resolve_url_or_path(p.to_str().unwrap()).unwrap(); - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_ok()); } @@ -1367,7 +1539,9 @@ mod tests { .join("tests/001_hello.js"); let specifier = ModuleSpecifier::resolve_url_or_path(p.to_str().unwrap()).unwrap(); - let r = fetcher.fetch_source_file(&specifier, None).await; + let r = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await; assert!(r.is_ok()); } @@ -1611,7 +1785,13 @@ mod tests { Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap(); let source = fetcher - .fetch_remote_source(&module_url, false, false, 1) + .fetch_remote_source( + &module_url, + false, + false, + 1, + &Permissions::allow_all(), + ) .await; assert!(source.is_ok()); let source = source.unwrap(); @@ -1633,7 +1813,13 @@ mod tests { let file_name = fetcher.http_cache.get_cache_filename(&module_url); let _ = fs::write(&file_name, "changed content"); let cached_source = fetcher - .fetch_remote_source(&module_url, false, false, 1) + .fetch_remote_source( + &module_url, + false, + false, + 1, + &Permissions::allow_all(), + ) .await .unwrap(); assert_eq!(cached_source.source_code, b"changed content"); @@ -1732,7 +1918,13 @@ mod tests { let module_url = Url::parse("http://127.0.0.1:4545/xTypeScriptTypes.js").unwrap(); let source = fetcher - .fetch_remote_source(&module_url, false, false, 1) + .fetch_remote_source( + &module_url, + false, + false, + 1, + &Permissions::allow_all(), + ) .await; assert!(source.is_ok()); let source = source.unwrap(); @@ -1752,7 +1944,13 @@ mod tests { let module_url = Url::parse("http://127.0.0.1:4545/referenceTypes.js").unwrap(); let source = fetcher - .fetch_remote_source(&module_url, false, false, 1) + .fetch_remote_source( + &module_url, + false, + false, + 1, + &Permissions::allow_all(), + ) .await; assert!(source.is_ok()); let source = source.unwrap(); diff --git a/cli/global_state.rs b/cli/global_state.rs index b91ba5b1089332..4e9bdbb99c9411 100644 --- a/cli/global_state.rs +++ b/cli/global_state.rs @@ -86,7 +86,6 @@ impl GlobalState { compiler_starts: AtomicUsize::new(0), compile_lock: AsyncMutex::new(()), }; - Ok(GlobalState(Arc::new(inner))) } @@ -95,6 +94,7 @@ impl GlobalState { module_specifier: ModuleSpecifier, maybe_referrer: Option, target_lib: TargetLib, + permissions: Permissions, ) -> Result { let state1 = self.clone(); let state2 = self.clone(); @@ -102,7 +102,7 @@ impl GlobalState { let out = self .file_fetcher - .fetch_source_file(&module_specifier, maybe_referrer) + .fetch_source_file(&module_specifier, maybe_referrer, permissions.clone()) .await?; // TODO(ry) Try to lift compile_lock as high up in the call stack for @@ -115,14 +115,14 @@ impl GlobalState { | msg::MediaType::JSX => { state1 .ts_compiler - .compile(state1.clone(), &out, target_lib) + .compile(state1.clone(), &out, target_lib, permissions.clone()) .await } msg::MediaType::JavaScript => { if state1.ts_compiler.compile_js { state2 .ts_compiler - .compile(state1.clone(), &out, target_lib) + .compile(state1.clone(), &out, target_lib, permissions.clone()) .await } else { if let Some(types_url) = out.types_url.clone() { @@ -132,6 +132,7 @@ impl GlobalState { .fetch_source_file( &types_specifier, Some(module_specifier.clone()), + permissions.clone(), ) .await .ok(); diff --git a/cli/lib.rs b/cli/lib.rs index 97b497e9566d6d..0b88d734609204 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -73,6 +73,7 @@ use crate::global_state::GlobalState; use crate::msg::MediaType; use crate::op_error::OpError; use crate::ops::io::get_stdio; +use crate::permissions::Permissions; use crate::state::DebugType; use crate::state::State; use crate::tsc::TargetLib; @@ -187,7 +188,7 @@ async fn print_file_info( let out = global_state .file_fetcher - .fetch_source_file(&module_specifier, None) + .fetch_source_file(&module_specifier, None, Permissions::allow_all()) .await?; println!( @@ -205,7 +206,12 @@ async fn print_file_info( let module_specifier_ = module_specifier.clone(); global_state .clone() - .fetch_compiled_module(module_specifier_, None, TargetLib::Main) + .fetch_compiled_module( + module_specifier_, + None, + TargetLib::Main, + Permissions::allow_all(), + ) .await?; if out.media_type == msg::MediaType::TypeScript @@ -407,7 +413,9 @@ async fn doc_command( let fetcher = self.clone(); async move { - let source_file = fetcher.fetch_source_file(&specifier, None).await?; + let source_file = fetcher + .fetch_source_file(&specifier, None, Permissions::allow_all()) + .await?; String::from_utf8(source_file.source_code) .map_err(|_| OpError::other("failed to parse".to_string())) } diff --git a/cli/ops/compiler.rs b/cli/ops/compiler.rs index 83b6d944c886c5..b844011877c390 100644 --- a/cli/ops/compiler.rs +++ b/cli/ops/compiler.rs @@ -69,7 +69,11 @@ fn op_fetch_source_files( None }; - let global_state = state.borrow().global_state.clone(); + let s = state.borrow(); + let global_state = s.global_state.clone(); + let permissions = s.permissions.clone(); + let perms_ = permissions.clone(); + drop(s); let file_fetcher = global_state.file_fetcher.clone(); let specifiers = args.specifiers.clone(); let future = async move { @@ -78,6 +82,7 @@ fn op_fetch_source_files( .map(|specifier| { let file_fetcher_ = file_fetcher.clone(); let ref_specifier_ = ref_specifier.clone(); + let perms_ = perms_.clone(); async move { let resolved_specifier = ModuleSpecifier::resolve_url(&specifier) .expect("Invalid specifier"); @@ -100,7 +105,7 @@ fn op_fetch_source_files( } } file_fetcher_ - .fetch_source_file(&resolved_specifier, ref_specifier_) + .fetch_source_file(&resolved_specifier, ref_specifier_, perms_) .await } .boxed_local() @@ -118,7 +123,11 @@ fn op_fetch_source_files( let types_specifier = ModuleSpecifier::from(types_url); global_state .file_fetcher - .fetch_source_file(&types_specifier, ref_specifier.clone()) + .fetch_source_file( + &types_specifier, + ref_specifier.clone(), + permissions.clone(), + ) .await .map_err(OpError::from)? } diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index e44d6fa8b2bd38..f3b741861a8668 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -30,10 +30,13 @@ fn op_compile( ) -> Result { state.check_unstable("Deno.compile"); let args: CompileArgs = serde_json::from_value(args)?; - let global_state = state.borrow().global_state.clone(); + let s = state.borrow(); + let global_state = s.global_state.clone(); + let permissions = s.permissions.clone(); let fut = async move { runtime_compile( global_state, + permissions, &args.root_name, &args.sources, args.bundle, @@ -58,9 +61,12 @@ fn op_transpile( ) -> Result { state.check_unstable("Deno.transpile"); let args: TranspileArgs = serde_json::from_value(args)?; - let global_state = state.borrow().global_state.clone(); + let s = state.borrow(); + let global_state = s.global_state.clone(); + let permissions = s.permissions.clone(); let fut = async move { - runtime_transpile(global_state, &args.sources, &args.options).await + runtime_transpile(global_state, permissions, &args.sources, &args.options) + .await } .boxed_local(); Ok(JsonOp::Async(fut)) diff --git a/cli/permissions.rs b/cli/permissions.rs index a56754f18d584d..bc4d0844e0ec63 100644 --- a/cli/permissions.rs +++ b/cli/permissions.rs @@ -134,6 +134,19 @@ impl Permissions { } } + pub fn allow_all() -> Self { + Self { + allow_read: PermissionState::from(true), + allow_write: PermissionState::from(true), + allow_net: PermissionState::from(true), + allow_env: PermissionState::from(true), + allow_run: PermissionState::from(true), + allow_plugin: PermissionState::from(true), + allow_hrtime: PermissionState::from(true), + ..Default::default() + } + } + pub fn check_run(&self) -> Result<(), OpError> { self .allow_run diff --git a/cli/state.rs b/cli/state.rs index 8c425d7005cd2a..9501ed2866f109 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -67,6 +67,7 @@ pub struct StateInner { pub seeded_rng: Option, pub target_lib: TargetLib, pub debug_type: DebugType, + pub is_main: bool, } impl State { @@ -314,9 +315,20 @@ impl ModuleLoader for State { let module_url_specified = module_specifier.to_string(); let global_state = state.global_state.clone(); let target_lib = state.target_lib.clone(); + let permissions = if state.is_main { + Permissions::allow_all() + } else { + state.permissions.clone() + }; + let fut = async move { let compiled_module = global_state - .fetch_compiled_module(module_specifier, maybe_referrer, target_lib) + .fetch_compiled_module( + module_specifier, + maybe_referrer, + target_lib, + permissions, + ) .await?; Ok(deno_core::ModuleSource { // Real module name, might be different from initial specifier @@ -395,6 +407,7 @@ impl State { seeded_rng, target_lib: TargetLib::Main, debug_type, + is_main: true, })); Ok(Self(state)) @@ -430,6 +443,7 @@ impl State { seeded_rng, target_lib: TargetLib::Worker, debug_type: DebugType::Dependent, + is_main: false, })); Ok(Self(state)) diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 4897422b9d7667..61e1fca7da32a2 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1019,6 +1019,7 @@ fn workers() { .arg("test") .arg("--reload") .arg("--allow-net") + .arg("--allow-read") .arg("--unstable") .arg("workers_test.ts") .spawn() @@ -1036,6 +1037,7 @@ fn compiler_api() { .arg("test") .arg("--unstable") .arg("--reload") + .arg("--allow-read") .arg("compiler_api_test.ts") .spawn() .unwrap() diff --git a/cli/tsc.rs b/cli/tsc.rs index f0b6172465fe8b..f207558a5c6f1b 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -11,6 +11,7 @@ use crate::global_state::GlobalState; use crate::msg; use crate::op_error::OpError; use crate::ops; +use crate::permissions::Permissions; use crate::source_maps::SourceMapGetter; use crate::startup_data; use crate::state::State; @@ -343,12 +344,19 @@ impl TsCompiler { /// Create a new V8 worker with snapshot of TS compiler and setup compiler's /// runtime. - fn setup_worker(global_state: GlobalState) -> CompilerWorker { + fn setup_worker( + global_state: GlobalState, + permissions: Permissions, + ) -> CompilerWorker { let entry_point = ModuleSpecifier::resolve_url_or_path("./__$deno$ts_compiler.ts").unwrap(); - let worker_state = - State::new(global_state.clone(), None, entry_point, DebugType::Internal) - .expect("Unable to create worker state"); + let worker_state = State::new( + global_state.clone(), + Some(permissions), + entry_point, + DebugType::Internal, + ) + .expect("Unable to create worker state"); // Count how many times we start the compiler worker. global_state.compiler_starts.fetch_add(1, Ordering::SeqCst); @@ -384,7 +392,9 @@ impl TsCompiler { global_state.flags.unstable, ); - let msg = execute_in_thread(global_state.clone(), req_msg).await?; + let permissions = Permissions::allow_all(); + let msg = + execute_in_thread(global_state.clone(), permissions, req_msg).await?; let json_str = std::str::from_utf8(&msg).unwrap(); debug!("Message: {}", json_str); @@ -440,6 +450,7 @@ impl TsCompiler { global_state: GlobalState, source_file: &SourceFile, target: TargetLib, + permissions: Permissions, ) -> Result { if self.has_compiled(&source_file.url) { return self.get_compiled_module(&source_file.url); @@ -492,7 +503,8 @@ impl TsCompiler { module_url.to_string() ); - let msg = execute_in_thread(global_state.clone(), req_msg).await?; + let msg = + execute_in_thread(global_state.clone(), permissions, req_msg).await?; let json_str = std::str::from_utf8(&msg).unwrap(); let compile_response: CompileResponse = serde_json::from_str(json_str)?; @@ -597,7 +609,7 @@ impl TsCompiler { ) -> std::io::Result<()> { let source_file = self .file_fetcher - .fetch_cached_source_file(&module_specifier) + .fetch_cached_source_file(&module_specifier, Permissions::allow_all()) .expect("Source file not found"); // NOTE: JavaScript files are only cached to disk if `checkJs` @@ -612,6 +624,10 @@ impl TsCompiler { .get_cache_filename_with_extension(module_specifier.as_url(), "js"); self.disk_cache.set(&js_key, contents.as_bytes())?; self.mark_compiled(module_specifier.as_url()); + let source_file = self + .file_fetcher + .fetch_cached_source_file(&module_specifier, Permissions::allow_all()) + .expect("Source file not found"); let version_hash = source_code_version_hash( &source_file.source_code, @@ -665,7 +681,7 @@ impl TsCompiler { ) -> std::io::Result<()> { let source_file = self .file_fetcher - .fetch_cached_source_file(&module_specifier) + .fetch_cached_source_file(&module_specifier, Permissions::allow_all()) .expect("Source file not found"); // NOTE: JavaScript files are only cached to disk if `checkJs` @@ -720,7 +736,7 @@ impl TsCompiler { if let Some(module_specifier) = self.try_to_resolve(script_name) { return self .file_fetcher - .fetch_cached_source_file(&module_specifier); + .fetch_cached_source_file(&module_specifier, Permissions::allow_all()); } None @@ -743,6 +759,7 @@ impl TsCompiler { async fn execute_in_thread( global_state: GlobalState, + permissions: Permissions, req: Buf, ) -> Result { let (handle_sender, handle_receiver) = @@ -750,7 +767,7 @@ async fn execute_in_thread( let builder = std::thread::Builder::new().name("deno-ts-compiler".to_string()); let join_handle = builder.spawn(move || { - let worker = TsCompiler::setup_worker(global_state.clone()); + let worker = TsCompiler::setup_worker(global_state.clone(), permissions); handle_sender.send(Ok(worker.thread_safe_handle())).unwrap(); drop(handle_sender); tokio_util::run_basic(worker).expect("Panic in event loop"); @@ -772,6 +789,7 @@ async fn execute_in_thread( /// This function is used by `Deno.compile()` and `Deno.bundle()` APIs. pub async fn runtime_compile( global_state: GlobalState, + permissions: Permissions, root_name: &str, sources: &Option>, bundle: bool, @@ -792,7 +810,7 @@ pub async fn runtime_compile( let compiler = global_state.ts_compiler.clone(); - let msg = execute_in_thread(global_state, req_msg).await?; + let msg = execute_in_thread(global_state, permissions, req_msg).await?; let json_str = std::str::from_utf8(&msg).unwrap(); // TODO(bartlomieju): factor `bundle` path into separate function `runtime_bundle` @@ -816,6 +834,7 @@ pub async fn runtime_compile( /// This function is used by `Deno.transpileOnly()` API. pub async fn runtime_transpile( global_state: GlobalState, + permissions: Permissions, sources: &HashMap, options: &Option, ) -> Result { @@ -828,7 +847,7 @@ pub async fn runtime_transpile( .into_boxed_str() .into_boxed_bytes(); - let msg = execute_in_thread(global_state, req_msg).await?; + let msg = execute_in_thread(global_state, permissions, req_msg).await?; let json_str = std::str::from_utf8(&msg).unwrap(); let v = serde_json::from_str::(json_str) .expect("Error decoding JSON string."); @@ -862,7 +881,12 @@ mod tests { GlobalState::mock(vec![String::from("deno"), String::from("hello.js")]); let result = mock_state .ts_compiler - .compile(mock_state.clone(), &out, TargetLib::Main) + .compile( + mock_state.clone(), + &out, + TargetLib::Main, + Permissions::allow_all(), + ) .await; assert!(result.is_ok()); assert!(result diff --git a/docs/runtime/compiler_apis.md b/docs/runtime/compiler_apis.md index 3a06b0b4affb31..30d583331c859e 100644 --- a/docs/runtime/compiler_apis.md +++ b/docs/runtime/compiler_apis.md @@ -15,9 +15,11 @@ fully qualified module name, and the value is the text source of the module. If `sources` is passed, Deno will resolve all the modules from within that hash and not attempt to resolve them outside of Deno. If `sources` are not provided, Deno will resolve modules as if the root module had been passed on the command line. -Deno will also cache any of these resources. The `options` argument is a set of -options of type `Deno.CompilerOptions`, which is a subset of the TypeScript -compiler options containing the ones supported by Deno. +Deno will also cache any of these resources. All resolved resources are treated +as dynamic imports and require read or net permissions depending if they're +local or remote. The `options` argument is a set of options of type +`Deno.CompilerOptions`, which is a subset of the TypeScript compiler options +containing the ones supported by Deno. The method resolves with a tuple. The first argument contains any diagnostics (syntax or type errors) related to the code. The second argument is a map where @@ -63,10 +65,12 @@ The `sources` is a hash where the key is the fully qualified module name, and the value is the text source of the module. If `sources` is passed, Deno will resolve all the modules from within that hash and not attempt to resolve them outside of Deno. If `sources` are not provided, Deno will resolve modules as if -the root module had been passed on the command line. Deno will also cache any of -these resources. The `options` argument is a set of options of type -`Deno.CompilerOptions`, which is a subset of the TypeScript compiler options -containing the ones supported by Deno. +the root module had been passed on the command line. All resolved resources are +treated as dynamic imports and require read or net permissions depending if +they're local or remote. Deno will also cache any of these resources. The +`options` argument is a set of options of type `Deno.CompilerOptions`, which is +a subset of the TypeScript compiler options containing the ones supported by +Deno. An example of providing sources: diff --git a/docs/runtime/workers.md b/docs/runtime/workers.md index 3c39d2cee7f6eb..16099a35bcacc1 100644 --- a/docs/runtime/workers.md +++ b/docs/runtime/workers.md @@ -18,6 +18,49 @@ new Worker("./worker.js"); new Worker("./worker.js", { type: "classic" }); ``` +### Permissions + +Creating a new `Worker` instance is similar to a dynamic import; therefore Deno +requires appropriate permission for this action. + +For workers using local modules; `--allow-read` permission is required: + +```ts +// main.ts +new Worker("./worker.ts", { type: "module" }); + +// worker.ts +console.log("hello world"); +self.close(); +``` + +```shell +$ deno run main.ts +error: Uncaught PermissionDenied: read access to "./worker.ts", run again with the --allow-read flag + +$ deno run --allow-read main.ts +hello world +``` + +For workers using remote modules; `--allow-read` permission is required: + +```ts +// main.ts +new Worker("https://example.com/worker.ts", { type: "module" }); + +// worker.ts +console.log("hello world"); +self.close(); +``` + +```shell +$ deno run main.ts +error: Uncaught PermissionDenied: net access to "https://example.com/worker.ts", run again with the --allow-net flag + +$ deno run --allow-net main.ts +hello world +``` + ### Using Deno in worker > This is an unstable Deno feature. Learn more about