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 /cli/args/flags.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 'cli/args/flags.rs')
-rw-r--r-- | cli/args/flags.rs | 253 |
1 files changed, 203 insertions, 50 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 10fa07bed..74ccb512f 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::collections::HashSet; use std::env; use std::ffi::OsString; @@ -44,6 +45,7 @@ use crate::args::resolve_no_prompt; use crate::util::fs::canonicalize_path; use super::flags_net; +use super::jsr_url; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum ConfigFlag { @@ -639,6 +641,7 @@ pub struct PermissionFlags { pub allow_write: Option<Vec<String>>, pub deny_write: Option<Vec<String>>, pub no_prompt: bool, + pub allow_import: Option<Vec<String>>, } impl PermissionFlags { @@ -658,9 +661,10 @@ impl PermissionFlags { || self.deny_sys.is_some() || self.allow_write.is_some() || self.deny_write.is_some() + || self.allow_import.is_some() } - pub fn to_options(&self) -> PermissionsOptions { + pub fn to_options(&self, cli_arg_urls: &[Cow<Url>]) -> PermissionsOptions { fn handle_allow<T: Default>( allow_all: bool, value: Option<T>, @@ -673,6 +677,41 @@ impl PermissionFlags { } } + fn handle_imports( + cli_arg_urls: &[Cow<Url>], + imports: Option<Vec<String>>, + ) -> Option<Vec<String>> { + if imports.is_some() { + return imports; + } + + let builtin_allowed_import_hosts = [ + "deno.land:443", + "esm.sh:443", + "jsr.io:443", + "raw.githubusercontent.com:443", + "gist.githubusercontent.com:443", + ]; + + let mut imports = + Vec::with_capacity(builtin_allowed_import_hosts.len() + 1); + imports + .extend(builtin_allowed_import_hosts.iter().map(|s| s.to_string())); + + // also add the JSR_URL env var + if let Some(jsr_host) = allow_import_host_from_url(jsr_url()) { + imports.push(jsr_host); + } + // include the cli arg urls + for url in cli_arg_urls { + if let Some(host) = allow_import_host_from_url(url) { + imports.push(host); + } + } + + Some(imports) + } + PermissionsOptions { allow_all: self.allow_all, allow_env: handle_allow(self.allow_all, self.allow_env.clone()), @@ -689,11 +728,33 @@ impl PermissionFlags { deny_sys: self.deny_sys.clone(), allow_write: handle_allow(self.allow_all, self.allow_write.clone()), deny_write: self.deny_write.clone(), + allow_import: handle_imports( + cli_arg_urls, + handle_allow(self.allow_all, self.allow_import.clone()), + ), prompt: !resolve_no_prompt(self), } } } +/// Gets the --allow-import host from the provided url +fn allow_import_host_from_url(url: &Url) -> Option<String> { + let host = url.host()?; + if let Some(port) = url.port() { + Some(format!("{}:{}", host, port)) + } else { + use deno_core::url::Host::*; + match host { + Domain(domain) if domain == "jsr.io" && url.scheme() == "https" => None, + _ => match url.scheme() { + "https" => Some(format!("{}:443", host)), + "http" => Some(format!("{}:80", host)), + _ => None, + }, + } + } +} + fn join_paths(allowlist: &[String], d: &str) -> String { allowlist .iter() @@ -881,6 +942,17 @@ impl Flags { _ => {} } + match &self.permissions.allow_import { + Some(allowlist) if allowlist.is_empty() => { + args.push("--allow-import".to_string()); + } + Some(allowlist) => { + let s = format!("--allow-import={}", allowlist.join(",")); + args.push(s); + } + _ => {} + } + args } @@ -991,6 +1063,7 @@ impl Flags { self.permissions.allow_write = None; self.permissions.allow_sys = None; self.permissions.allow_ffi = None; + self.permissions.allow_import = None; } pub fn resolve_watch_exclude_set( @@ -1707,6 +1780,7 @@ Future runs of this module will trigger no downloads or compilation unless --rel ) .arg(frozen_lockfile_arg()) .arg(allow_scripts_arg()) + .arg(allow_import_arg()) }) } @@ -1766,6 +1840,7 @@ Unless --reload is specified, this command will not re-download already cached d .required_unless_present("help") .value_hint(ValueHint::FilePath), ) + .arg(allow_import_arg()) } ) } @@ -1994,6 +2069,7 @@ Show documentation for runtime built-ins: .arg(no_lock_arg()) .arg(no_npm_arg()) .arg(no_remote_arg()) + .arg(allow_import_arg()) .arg( Arg::new("json") .long("json") @@ -2358,6 +2434,7 @@ The following information is shown: .help("UNSTABLE: Outputs the information in JSON format") .action(ArgAction::SetTrue), )) + .arg(allow_import_arg()) } fn install_subcommand() -> Command { @@ -3151,47 +3228,44 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .after_help(cstr!(r#"<y>Permission options:</> <y>Docs</>: <c>https://docs.deno.com/go/permissions</> - <g>-A, --allow-all</> Allow all permissions. - <g>--no-prompt</> Always throw if required permission wasn't passed. - <p(245)>Can also be set via the DENO_NO_PROMPT environment variable.</> - <g>-R, --allow-read[=<<PATH>...]</> Allow file system read access. Optionally specify allowed paths. - <p(245)>--allow-read | --allow-read="/etc,/var/log.txt"</> - <g>-W, --allow-write[=<<PATH>...]</> Allow file system write access. Optionally specify allowed paths. - <p(245)>--allow-write | --allow-write="/etc,/var/log.txt"</> - <g>-N, --allow-net[=<<IP_OR_HOSTNAME>...]</> Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary. - <p(245)>--allow-net | --allow-net="localhost:8080,deno.land"</> - <g>-E, --allow-env[=<<VARIABLE_NAME>...]</> Allow access to environment variables. Optionally specify accessible environment variables. - <p(245)>--allow-env | --allow-env="PORT,HOME,PATH"</> - <g>-S, --allow-sys[=<<API_NAME>...]</> Allow access to OS information. Optionally allow specific APIs by function name. - <p(245)>--allow-sys | --allow-sys="systemMemoryInfo,osRelease"</> - <g>--allow-run[=<<PROGRAM_NAME>...]</> Allow running subprocesses. Optionally specify allowed runnable program names. - <p(245)>--allow-run | --allow-run="whoami,ps"</> - <g>--allow-ffi[=<<PATH>...]</> (Unstable) Allow loading dynamic libraries. Optionally specify allowed directories or files. - <p(245)>--allow-ffi | --allow-ffi="./libfoo.so"</> - <g> --deny-read[=<<PATH>...]</> Deny file system read access. Optionally specify denied paths. - <p(245)>--deny-read | --deny-read="/etc,/var/log.txt"</> - <g> --deny-write[=<<PATH>...]</> Deny file system write access. Optionally specify denied paths. - <p(245)>--deny-write | --deny-write="/etc,/var/log.txt"</> - <g> --deny-net[=<<IP_OR_HOSTNAME>...]</> Deny network access. Optionally specify defined IP addresses and host names, with ports as necessary. - <p(245)>--deny-net | --deny-net="localhost:8080,deno.land"</> - <g> --deny-env[=<<VARIABLE_NAME>...]</> Deny access to environment variables. Optionally specify inacessible environment variables. - <p(245)>--deny-env | --deny-env="PORT,HOME,PATH"</> - <g>-S, --deny-sys[=<<API_NAME>...]</> Deny access to OS information. Optionally deny specific APIs by function name. - <p(245)>--deny-sys | --deny-sys="systemMemoryInfo,osRelease"</> - <g>--deny-run[=<<PROGRAM_NAME>...]</> Deny running subprocesses. Optionally specify denied runnable program names. - <p(245)>--deny-run | --deny-run="whoami,ps"</> - <g>--deny-ffi[=<<PATH>...]</> (Unstable) Deny loading dynamic libraries. Optionally specify denied directories or files. - <p(245)>--deny-ffi | --deny-ffi="./libfoo.so"</> + <g>-A, --allow-all</> Allow all permissions. + <g>--no-prompt</> Always throw if required permission wasn't passed. + <p(245)>Can also be set via the DENO_NO_PROMPT environment variable.</> + <g>-R, --allow-read[=<<PATH>...]</> Allow file system read access. Optionally specify allowed paths. + <p(245)>--allow-read | --allow-read="/etc,/var/log.txt"</> + <g>-W, --allow-write[=<<PATH>...]</> Allow file system write access. Optionally specify allowed paths. + <p(245)>--allow-write | --allow-write="/etc,/var/log.txt"</> + <g>-I, --allow-import[=<<IP_OR_HOSTNAME>...]</> Allow importing from remote hosts. Optionally specify allowed IP addresses and host names, with ports as necessary. + Default value: <p(245)>deno.land:443,jsr.io:443,esm.sh:443,raw.githubusercontent.com:443,user.githubusercontent.com:443</> + <p(245)>--allow-import | --allow-import="example.com,github.com"</> + <g>-N, --allow-net[=<<IP_OR_HOSTNAME>...]</> Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary. + <p(245)>--allow-net | --allow-net="localhost:8080,deno.land"</> + <g>-E, --allow-env[=<<VARIABLE_NAME>...]</> Allow access to environment variables. Optionally specify accessible environment variables. + <p(245)>--allow-env | --allow-env="PORT,HOME,PATH"</> + <g>-S, --allow-sys[=<<API_NAME>...]</> Allow access to OS information. Optionally allow specific APIs by function name. + <p(245)>--allow-sys | --allow-sys="systemMemoryInfo,osRelease"</> + <g>--allow-run[=<<PROGRAM_NAME>...]</> Allow running subprocesses. Optionally specify allowed runnable program names. + <p(245)>--allow-run | --allow-run="whoami,ps"</> + <g>--allow-ffi[=<<PATH>...]</> (Unstable) Allow loading dynamic libraries. Optionally specify allowed directories or files. + <p(245)>--allow-ffi | --allow-ffi="./libfoo.so"</> + <g> --deny-read[=<<PATH>...]</> Deny file system read access. Optionally specify denied paths. + <p(245)>--deny-read | --deny-read="/etc,/var/log.txt"</> + <g> --deny-write[=<<PATH>...]</> Deny file system write access. Optionally specify denied paths. + <p(245)>--deny-write | --deny-write="/etc,/var/log.txt"</> + <g> --deny-net[=<<IP_OR_HOSTNAME>...]</> Deny network access. Optionally specify defined IP addresses and host names, with ports as necessary. + <p(245)>--deny-net | --deny-net="localhost:8080,deno.land"</> + <g> --deny-env[=<<VARIABLE_NAME>...]</> Deny access to environment variables. Optionally specify inacessible environment variables. + <p(245)>--deny-env | --deny-env="PORT,HOME,PATH"</> + <g>-S, --deny-sys[=<<API_NAME>...]</> Deny access to OS information. Optionally deny specific APIs by function name. + <p(245)>--deny-sys | --deny-sys="systemMemoryInfo,osRelease"</> + <g>--deny-run[=<<PROGRAM_NAME>...]</> Deny running subprocesses. Optionally specify denied runnable program names. + <p(245)>--deny-run | --deny-run="whoami,ps"</> + <g>--deny-ffi[=<<PATH>...]</> (Unstable) Deny loading dynamic libraries. Optionally specify denied directories or files. + <p(245)>--deny-ffi | --deny-ffi="./libfoo.so"</> "#)) .arg( { - let mut arg = Arg::new("allow-all") - .short('A') - .long("allow-all") - .action(ArgAction::SetTrue) - .help("Allow all permissions") - .hide(true) - ; + let mut arg = allow_all_arg().hide(true); if let Some(requires) = requires { arg = arg.requires(requires) } @@ -3200,7 +3274,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("allow-read") + let mut arg = Arg::new("allow-read") .long("allow-read") .short('R') .num_args(0..) @@ -3218,7 +3292,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("deny-read") + let mut arg = Arg::new("deny-read") .long("deny-read") .num_args(0..) .action(ArgAction::Append) @@ -3235,7 +3309,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("allow-write") + let mut arg = Arg::new("allow-write") .long("allow-write") .short('W') .num_args(0..) @@ -3253,7 +3327,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("deny-write") + let mut arg = Arg::new("deny-write") .long("deny-write") .num_args(0..) .action(ArgAction::Append) @@ -3270,7 +3344,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("allow-net") + let mut arg = Arg::new("allow-net") .long("allow-net") .short('N') .num_args(0..) @@ -3289,7 +3363,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("deny-net") + let mut arg = Arg::new("deny-net") .long("deny-net") .num_args(0..) .use_value_delimiter(true) @@ -3383,7 +3457,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("deny-sys") + let mut arg = Arg::new("deny-sys") .long("deny-sys") .num_args(0..) .use_value_delimiter(true) @@ -3418,7 +3492,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { ) .arg( { - let mut arg = Arg::new("deny-run") + let mut arg = Arg::new("deny-run") .long("deny-run") .num_args(0..) .use_value_delimiter(true) @@ -3509,6 +3583,26 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { arg } ) + .arg( + { + let mut arg = allow_import_arg().hide(true); + if let Some(requires) = requires { + // allow this for install --global + if requires != "global" { + arg = arg.requires(requires) + } + } + arg + } + ) +} + +fn allow_all_arg() -> Arg { + Arg::new("allow-all") + .short('A') + .long("allow-all") + .action(ArgAction::SetTrue) + .help("Allow all permissions") } fn runtime_args( @@ -3537,6 +3631,20 @@ fn runtime_args( .arg(strace_ops_arg()) } +fn allow_import_arg() -> Arg { + Arg::new("allow-import") + .long("allow-import") + .short('I') + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("IP_OR_HOSTNAME") + .help(cstr!( + "Allow importing from remote hosts. Optionally specify allowed IP addresses and host names, with ports as necessary. Default value: <p(245)>deno.land:443,jsr.io:443,esm.sh:443,raw.githubusercontent.com:443,user.githubusercontent.com:443</>" + )) + .value_parser(flags_net::validator) +} + fn inspect_args(app: Command) -> Command { app .arg( @@ -4174,6 +4282,7 @@ fn cache_parse( unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionOnly); frozen_lockfile_arg_parse(flags, matches); allow_scripts_arg_parse(flags, matches)?; + allow_import_parse(flags, matches); let files = matches.remove_many::<String>("file").unwrap().collect(); flags.subcommand = DenoSubcommand::Cache(CacheFlags { files }); Ok(()) @@ -4195,6 +4304,7 @@ fn check_parse( doc: matches.get_flag("doc"), doc_only: matches.get_flag("doc-only"), }); + allow_import_parse(flags, matches); Ok(()) } @@ -4320,6 +4430,7 @@ fn doc_parse( no_lock_arg_parse(flags, matches); no_npm_arg_parse(flags, matches); no_remote_arg_parse(flags, matches); + allow_import_parse(flags, matches); let source_files_val = matches.remove_many::<String>("source_file"); let source_files = if let Some(val) = source_files_val { @@ -4460,6 +4571,7 @@ fn info_parse( lock_args_parse(flags, matches); no_remote_arg_parse(flags, matches); no_npm_arg_parse(flags, matches); + allow_import_parse(flags, matches); let json = matches.get_flag("json"); flags.subcommand = DenoSubcommand::Info(InfoFlags { file: matches.remove_one::<String>("file"), @@ -4495,6 +4607,7 @@ fn install_parse( force, }), }); + return Ok(()); } @@ -5175,13 +5288,22 @@ fn permission_args_parse( } if matches.get_flag("allow-hrtime") || matches.get_flag("deny-hrtime") { - log::warn!("⚠️ Warning: `allow-hrtime` and `deny-hrtime` have been removed in Deno 2, as high resolution time is now always allowed."); + // use eprintln instead of log::warn because logging hasn't been initialized yet + #[allow(clippy::print_stderr)] + { + eprintln!( + "{} `allow-hrtime` and `deny-hrtime` have been removed in Deno 2, as high resolution time is now always allowed", + deno_runtime::colors::yellow("Warning") + ); + } } if matches.get_flag("allow-all") { flags.allow_all(); } + allow_import_parse(flags, matches); + if matches.get_flag("no-prompt") { flags.permissions.no_prompt = true; } @@ -5189,6 +5311,13 @@ fn permission_args_parse( Ok(()) } +fn allow_import_parse(flags: &mut Flags, matches: &mut ArgMatches) { + if let Some(imports_wl) = matches.remove_many::<String>("allow-import") { + let imports_allowlist = flags_net::parse(imports_wl.collect()).unwrap(); + flags.permissions.allow_import = Some(imports_allowlist); + } +} + fn unsafely_ignore_certificate_errors_parse( flags: &mut Flags, matches: &mut ArgMatches, @@ -6215,7 +6344,7 @@ mod tests { #[test] fn short_permission_flags() { - let r = flags_from_vec(svec!["deno", "run", "-RNESW", "gist.ts"]); + let r = flags_from_vec(svec!["deno", "run", "-RNESWI", "gist.ts"]); assert_eq!( r.unwrap(), Flags { @@ -6226,6 +6355,7 @@ mod tests { allow_read: Some(vec![]), allow_write: Some(vec![]), allow_env: Some(vec![]), + allow_import: Some(vec![]), allow_net: Some(vec![]), allow_sys: Some(vec![]), ..Default::default() @@ -10777,7 +10907,7 @@ mod tests { } ); // just make sure this doesn't panic - let _ = flags.permissions.to_options(); + let _ = flags.permissions.to_options(&[]); } #[test] @@ -10852,4 +10982,27 @@ mod tests { Usage: deno repl [OPTIONS] [-- [ARGS]...]\n" ) } + + #[test] + fn test_allow_import_host_from_url() { + fn parse(text: &str) -> Option<String> { + allow_import_host_from_url(&Url::parse(text).unwrap()) + } + + assert_eq!(parse("https://jsr.io"), None); + assert_eq!( + parse("http://127.0.0.1:4250"), + Some("127.0.0.1:4250".to_string()) + ); + assert_eq!(parse("http://jsr.io"), Some("jsr.io:80".to_string())); + assert_eq!( + parse("https://example.com"), + Some("example.com:443".to_string()) + ); + assert_eq!( + parse("http://example.com"), + Some("example.com:80".to_string()) + ); + assert_eq!(parse("file:///example.com"), None); + } } |