From 6fb7e8d93bb9fd8cdd81130a394ae6061930c4f6 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Thu, 3 Aug 2023 21:19:19 +1000 Subject: feat(permissions): add "--deny-*" flags (#19070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds new "--deny-*" permission flags. These are complimentary to "--allow-*" flags. These flags can be used to restrict access to certain resources, even if they were granted using "--allow-*" flags or the "--allow-all" ("-A") flag. Eg. specifying "--allow-read --deny-read" will result in a permission error, while "--allow-read --deny-read=/etc" will allow read access to all FS but the "/etc" directory. Runtime permissions APIs ("Deno.permissions") were adjusted as well, mainly by adding, a new "PermissionStatus.partial" field. This field denotes that while permission might be granted to requested resource, it's only partial (ie. a "--deny-*" flag was specified that excludes some of the requested resources). Eg. specifying "--allow-read=foo/ --deny-read=foo/bar" and then querying for permissions like "Deno.permissions.query({ name: "read", path: "foo/" })" will return "PermissionStatus { state: "granted", onchange: null, partial: true }", denoting that some of the subpaths don't have read access. Closes #18804. --------- Co-authored-by: Bartek IwaƄczuk Co-authored-by: Nayeem Rahman --- cli/args/flags.rs | 615 +++++++++++++++++++++++++++++++++++++++++++- cli/args/flags_allow_net.rs | 202 --------------- cli/args/flags_net.rs | 202 +++++++++++++++ cli/args/mod.rs | 10 +- 4 files changed, 821 insertions(+), 208 deletions(-) delete mode 100644 cli/args/flags_allow_net.rs create mode 100644 cli/args/flags_net.rs (limited to 'cli/args') diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 9674c68a6..790a9d83f 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -24,7 +24,7 @@ use std::str::FromStr; use crate::util::fs::canonicalize_path; -use super::flags_allow_net; +use super::flags_net; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct FileFlags { @@ -364,13 +364,21 @@ pub struct Flags { pub allow_all: bool, pub allow_env: Option>, + pub deny_env: Option>, pub allow_hrtime: bool, + pub deny_hrtime: bool, pub allow_net: Option>, + pub deny_net: Option>, pub allow_ffi: Option>, + pub deny_ffi: Option>, pub allow_read: Option>, + pub deny_read: Option>, pub allow_run: Option>, + pub deny_run: Option>, pub allow_sys: Option>, + pub deny_sys: Option>, pub allow_write: Option>, + pub deny_write: Option>, pub ca_stores: Option>, pub ca_data: Option, pub cache_blocklist: Vec, @@ -434,6 +442,17 @@ impl Flags { _ => {} } + match &self.deny_read { + Some(read_denylist) if read_denylist.is_empty() => { + args.push("--deny-read".to_string()); + } + Some(read_denylist) => { + let s = format!("--deny-read={}", join_paths(read_denylist, ",")); + args.push(s); + } + _ => {} + } + match &self.allow_write { Some(write_allowlist) if write_allowlist.is_empty() => { args.push("--allow-write".to_string()); @@ -445,6 +464,17 @@ impl Flags { _ => {} } + match &self.deny_write { + Some(write_denylist) if write_denylist.is_empty() => { + args.push("--deny-write".to_string()); + } + Some(write_denylist) => { + let s = format!("--deny-write={}", join_paths(write_denylist, ",")); + args.push(s); + } + _ => {} + } + match &self.allow_net { Some(net_allowlist) if net_allowlist.is_empty() => { args.push("--allow-net".to_string()); @@ -456,6 +486,17 @@ impl Flags { _ => {} } + match &self.deny_net { + Some(net_denylist) if net_denylist.is_empty() => { + args.push("--deny-net".to_string()); + } + Some(net_denylist) => { + let s = format!("--deny-net={}", net_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.unsafely_ignore_certificate_errors { Some(ic_allowlist) if ic_allowlist.is_empty() => { args.push("--unsafely-ignore-certificate-errors".to_string()); @@ -481,6 +522,17 @@ impl Flags { _ => {} } + match &self.deny_env { + Some(env_denylist) if env_denylist.is_empty() => { + args.push("--deny-env".to_string()); + } + Some(env_denylist) => { + let s = format!("--deny-env={}", env_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.allow_run { Some(run_allowlist) if run_allowlist.is_empty() => { args.push("--allow-run".to_string()); @@ -492,6 +544,17 @@ impl Flags { _ => {} } + match &self.deny_run { + Some(run_denylist) if run_denylist.is_empty() => { + args.push("--deny-run".to_string()); + } + Some(run_denylist) => { + let s = format!("--deny-run={}", run_denylist.join(",")); + args.push(s); + } + _ => {} + } + match &self.allow_sys { Some(sys_allowlist) if sys_allowlist.is_empty() => { args.push("--allow-sys".to_string()); @@ -503,6 +566,17 @@ impl Flags { _ => {} } + match &self.deny_sys { + Some(sys_denylist) if sys_denylist.is_empty() => { + args.push("--deny-sys".to_string()); + } + Some(sys_denylist) => { + let s = format!("--deny-sys={}", sys_denylist.join(",")); + args.push(s) + } + _ => {} + } + match &self.allow_ffi { Some(ffi_allowlist) if ffi_allowlist.is_empty() => { args.push("--allow-ffi".to_string()); @@ -514,10 +588,25 @@ impl Flags { _ => {} } + match &self.deny_ffi { + Some(ffi_denylist) if ffi_denylist.is_empty() => { + args.push("--deny-ffi".to_string()); + } + Some(ffi_denylist) => { + let s = format!("--deny-ffi={}", join_paths(ffi_denylist, ",")); + args.push(s); + } + _ => {} + } + if self.allow_hrtime { args.push("--allow-hrtime".to_string()); } + if self.deny_hrtime { + args.push("--deny-hrtime".to_string()); + } + args } @@ -607,26 +696,42 @@ impl Flags { pub fn has_permission(&self) -> bool { self.allow_all || self.allow_hrtime + || self.deny_hrtime || self.allow_env.is_some() + || self.deny_env.is_some() || self.allow_ffi.is_some() + || self.deny_ffi.is_some() || self.allow_net.is_some() + || self.deny_net.is_some() || self.allow_read.is_some() + || self.deny_read.is_some() || self.allow_run.is_some() + || self.deny_run.is_some() || self.allow_sys.is_some() + || self.deny_sys.is_some() || self.allow_write.is_some() + || self.deny_write.is_some() } pub fn has_permission_in_argv(&self) -> bool { self.argv.iter().any(|arg| { arg == "--allow-all" || arg == "--allow-hrtime" + || arg == "--deny-hrtime" || arg.starts_with("--allow-env") + || arg.starts_with("--deny-env") || arg.starts_with("--allow-ffi") + || arg.starts_with("--deny-ffi") || arg.starts_with("--allow-net") + || arg.starts_with("--deny-net") || arg.starts_with("--allow-read") + || arg.starts_with("--deny-read") || arg.starts_with("--allow-run") + || arg.starts_with("--deny-run") || arg.starts_with("--allow-sys") + || arg.starts_with("--deny-sys") || arg.starts_with("--allow-write") + || arg.starts_with("--deny-write") }) } } @@ -2037,6 +2142,16 @@ static ALLOW_READ_HELP: &str = concat!( " --allow-read=\"/etc,/var/log.txt\"" ); +static DENY_READ_HELP: &str = concat!( + "Deny file system read access. Optionally specify denied paths.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-read\n", + " --deny-read=\"/etc,/var/log.txt\"" +); + static ALLOW_WRITE_HELP: &str = concat!( "Allow file system write access. Optionally specify allowed paths.\n", "Docs: https://deno.land/manual@v", @@ -2047,6 +2162,16 @@ static ALLOW_WRITE_HELP: &str = concat!( " --allow-write=\"/etc,/var/log.txt\"" ); +static DENY_WRITE_HELP: &str = concat!( + "Deny file system write access. Optionally specify denied paths.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-write\n", + " --deny-write=\"/etc,/var/log.txt\"" +); + static ALLOW_NET_HELP: &str = concat!( "Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary.\n", "Docs: https://deno.land/manual@v", @@ -2057,6 +2182,16 @@ static ALLOW_NET_HELP: &str = concat!( " --allow-net=\"localhost:8080,deno.land\"" ); +static DENY_NET_HELP: &str = concat!( + "Deny network access. Optionally specify denied IP addresses and host names, with ports as necessary.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-net\n", + " --deny-net=\"localhost:8080,deno.land\"" +); + static ALLOW_ENV_HELP: &str = concat!( "Allow access to system environment information. Optionally specify accessible environment variables.\n", "Docs: https://deno.land/manual@v", @@ -2067,6 +2202,16 @@ static ALLOW_ENV_HELP: &str = concat!( " --allow-env=\"PORT,HOME,PATH\"" ); +static DENY_ENV_HELP: &str = concat!( + "Deny access to system environment information. Optionally specify accessible environment variables.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-env\n", + " --deny-env=\"PORT,HOME,PATH\"" +); + static ALLOW_SYS_HELP: &str = concat!( "Allow access to OS information. Optionally allow specific APIs by function name.\n", "Docs: https://deno.land/manual@v", @@ -2077,6 +2222,16 @@ static ALLOW_SYS_HELP: &str = concat!( " --allow-sys=\"systemMemoryInfo,osRelease\"" ); +static DENY_SYS_HELP: &str = concat!( + "Deny access to OS information. Optionally deny specific APIs by function name.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-sys\n", + " --deny-sys=\"systemMemoryInfo,osRelease\"" +); + static ALLOW_RUN_HELP: &str = concat!( "Allow running subprocesses. Optionally specify allowed runnable program names.\n", "Docs: https://deno.land/manual@v", @@ -2087,6 +2242,16 @@ static ALLOW_RUN_HELP: &str = concat!( " --allow-run=\"whoami,ps\"" ); +static DENY_RUN_HELP: &str = concat!( + "Deny running subprocesses. Optionally specify denied runnable program names.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-run\n", + " --deny-run=\"whoami,ps\"" +); + static ALLOW_FFI_HELP: &str = concat!( "(Unstable) Allow loading dynamic libraries. Optionally specify allowed directories or files.\n", "Docs: https://deno.land/manual@v", @@ -2097,6 +2262,16 @@ static ALLOW_FFI_HELP: &str = concat!( " --allow-ffi=\"./libfoo.so\"" ); +static DENY_FFI_HELP: &str = concat!( + "(Unstable) Deny loading dynamic libraries. Optionally specify denied directories or files.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n", + "Examples:\n", + " --deny-ffi\n", + " --deny-ffi=\"./libfoo.so\"" +); + static ALLOW_HRTIME_HELP: &str = concat!( "Allow high-resolution time measurement. Note: this can enable timing attacks and fingerprinting.\n", "Docs: https://deno.land/manual@v", @@ -2104,6 +2279,13 @@ static ALLOW_HRTIME_HELP: &str = concat!( "/basics/permissions\n" ); +static DENY_HRTIME_HELP: &str = concat!( + "Deny high-resolution time measurement. Note: this can prevent timing attacks and fingerprinting.\n", + "Docs: https://deno.land/manual@v", + env!("CARGO_PKG_VERSION"), + "/basics/permissions\n" +); + static ALLOW_ALL_HELP: &str = concat!( "Allow all permissions. Learn more about permissions in Deno:\n", "https://deno.land/manual@v", @@ -2124,6 +2306,17 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-read") + .long("deny-read") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_READ_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-write") .long("allow-write") @@ -2135,6 +2328,17 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-write") + .long("deny-write") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_WRITE_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-net") .long("allow-net") @@ -2143,7 +2347,17 @@ fn permission_args(app: Command) -> Command { .require_equals(true) .value_name("IP_OR_HOSTNAME") .help(ALLOW_NET_HELP) - .value_parser(flags_allow_net::validator), + .value_parser(flags_net::validator), + ) + .arg( + Arg::new("deny-net") + .long("deny-net") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("IP_OR_HOSTNAME") + .help(DENY_NET_HELP) + .value_parser(flags_net::validator), ) .arg(unsafely_ignore_certificate_errors_arg()) .arg( @@ -2166,6 +2380,26 @@ fn permission_args(app: Command) -> Command { }) }), ) + .arg( + Arg::new("deny-env") + .long("deny-env") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("VARIABLE_NAME") + .help(DENY_ENV_HELP) + .value_parser(|key: &str| { + if key.is_empty() || key.contains(&['=', '\0'] as &[char]) { + return Err(format!("invalid key \"{key}\"")); + } + + Ok(if cfg!(windows) { + key.to_uppercase() + } else { + key.to_string() + }) + }), + ) .arg( Arg::new("allow-sys") .long("allow-sys") @@ -2176,6 +2410,16 @@ fn permission_args(app: Command) -> Command { .help(ALLOW_SYS_HELP) .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)), ) + .arg( + Arg::new("deny-sys") + .long("deny-sys") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("API_NAME") + .help(DENY_SYS_HELP) + .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)), + ) .arg( Arg::new("allow-run") .long("allow-run") @@ -2185,6 +2429,15 @@ fn permission_args(app: Command) -> Command { .value_name("PROGRAM_NAME") .help(ALLOW_RUN_HELP), ) + .arg( + Arg::new("deny-run") + .long("deny-run") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PROGRAM_NAME") + .help(DENY_RUN_HELP), + ) .arg( Arg::new("allow-ffi") .long("allow-ffi") @@ -2196,12 +2449,29 @@ fn permission_args(app: Command) -> Command { .value_parser(value_parser!(PathBuf)) .value_hint(ValueHint::AnyPath), ) + .arg( + Arg::new("deny-ffi") + .long("deny-ffi") + .num_args(0..) + .use_value_delimiter(true) + .require_equals(true) + .value_name("PATH") + .help(DENY_FFI_HELP) + .value_parser(value_parser!(PathBuf)) + .value_hint(ValueHint::AnyPath), + ) .arg( Arg::new("allow-hrtime") .long("allow-hrtime") .action(ArgAction::SetTrue) .help(ALLOW_HRTIME_HELP), ) + .arg( + Arg::new("deny-hrtime") + .long("deny-hrtime") + .action(ArgAction::SetTrue) + .help(DENY_HRTIME_HELP), + ) .arg( Arg::new("allow-all") .short('A') @@ -2594,7 +2864,7 @@ fn unsafely_ignore_certificate_errors_arg() -> Arg { .require_equals(true) .value_name("HOSTNAMES") .help("DANGER: Disables verification of TLS certificates") - .value_parser(flags_allow_net::validator) + .value_parser(flags_net::validator) } fn bench_parse(flags: &mut Flags, matches: &mut ArgMatches) { @@ -3189,38 +3459,76 @@ fn permission_args_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.allow_read = Some(read_wl.collect()); } + if let Some(read_wl) = matches.remove_many::("deny-read") { + flags.deny_read = Some(read_wl.collect()); + } + if let Some(write_wl) = matches.remove_many::("allow-write") { flags.allow_write = Some(write_wl.collect()); } + if let Some(write_wl) = matches.remove_many::("deny-write") { + flags.deny_write = Some(write_wl.collect()); + } + if let Some(net_wl) = matches.remove_many::("allow-net") { - let net_allowlist = flags_allow_net::parse(net_wl.collect()).unwrap(); + let net_allowlist = flags_net::parse(net_wl.collect()).unwrap(); flags.allow_net = Some(net_allowlist); } + if let Some(net_wl) = matches.remove_many::("deny-net") { + let net_denylist = flags_net::parse(net_wl.collect()).unwrap(); + flags.deny_net = Some(net_denylist); + } + if let Some(env_wl) = matches.remove_many::("allow-env") { flags.allow_env = Some(env_wl.collect()); debug!("env allowlist: {:#?}", &flags.allow_env); } + if let Some(env_wl) = matches.remove_many::("deny-env") { + flags.deny_env = Some(env_wl.collect()); + debug!("env denylist: {:#?}", &flags.deny_env); + } + if let Some(run_wl) = matches.remove_many::("allow-run") { flags.allow_run = Some(run_wl.collect()); debug!("run allowlist: {:#?}", &flags.allow_run); } + if let Some(run_wl) = matches.remove_many::("deny-run") { + flags.deny_run = Some(run_wl.collect()); + debug!("run denylist: {:#?}", &flags.deny_run); + } + if let Some(sys_wl) = matches.remove_many::("allow-sys") { flags.allow_sys = Some(sys_wl.collect()); debug!("sys info allowlist: {:#?}", &flags.allow_sys); } + if let Some(sys_wl) = matches.remove_many::("deny-sys") { + flags.deny_sys = Some(sys_wl.collect()); + debug!("sys info denylist: {:#?}", &flags.deny_sys); + } + if let Some(ffi_wl) = matches.remove_many::("allow-ffi") { flags.allow_ffi = Some(ffi_wl.collect()); debug!("ffi allowlist: {:#?}", &flags.allow_ffi); } + if let Some(ffi_wl) = matches.remove_many::("deny-ffi") { + flags.deny_ffi = Some(ffi_wl.collect()); + debug!("ffi denylist: {:#?}", &flags.deny_ffi); + } + if matches.get_flag("allow-hrtime") { flags.allow_hrtime = true; } + + if matches.get_flag("deny-hrtime") { + flags.deny_hrtime = true; + } + if matches.get_flag("allow-all") { flags.allow_all = true; flags.allow_read = Some(vec![]); @@ -3232,6 +3540,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; } + if matches.get_flag("no-prompt") { flags.no_prompt = true; } @@ -3243,7 +3552,7 @@ fn unsafely_ignore_certificate_errors_parse( if let Some(ic_wl) = matches.remove_many::("unsafely-ignore-certificate-errors") { - let ic_allowlist = flags_allow_net::parse(ic_wl.collect()).unwrap(); + let ic_allowlist = flags_net::parse(ic_wl.collect()).unwrap(); flags.unsafely_ignore_certificate_errors = Some(ic_allowlist); } } @@ -3694,6 +4003,9 @@ mod tests { let r = flags_from_vec(svec!["deno", "run", "--allow-read", "x.ts"]); assert_eq!(r.unwrap().has_permission(), true); + let r = flags_from_vec(svec!["deno", "run", "--deny-read", "x.ts"]); + assert_eq!(r.unwrap().has_permission(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts"]); assert_eq!(r.unwrap().has_permission(), false); } @@ -3703,6 +4015,9 @@ mod tests { let r = flags_from_vec(svec!["deno", "run", "x.ts", "--allow-read"]); assert_eq!(r.unwrap().has_permission_in_argv(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts", "--deny-read"]); + assert_eq!(r.unwrap().has_permission_in_argv(), true); + let r = flags_from_vec(svec!["deno", "run", "x.ts"]); assert_eq!(r.unwrap().has_permission_in_argv(), false); } @@ -3771,6 +4086,22 @@ mod tests { ); } + #[test] + fn deny_read() { + let r = flags_from_vec(svec!["deno", "run", "--deny-read", "gist.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "gist.ts".to_string(), + }), + deny_read: Some(vec![]), + ..Flags::default() + } + ); + } + #[test] fn allow_hrtime() { let r = flags_from_vec(svec!["deno", "run", "--allow-hrtime", "gist.ts"]); @@ -3787,6 +4118,22 @@ mod tests { ); } + #[test] + fn deny_hrtime() { + let r = flags_from_vec(svec!["deno", "run", "--deny-hrtime", "gist.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "gist.ts".to_string(), + }), + deny_hrtime: true, + ..Flags::default() + } + ); + } + #[test] fn double_hyphen() { // notice that flags passed after double dash will not @@ -4687,11 +5034,17 @@ mod tests { allow_net: Some(vec![]), unsafely_ignore_certificate_errors: None, allow_env: Some(vec![]), + deny_env: None, allow_run: Some(vec![]), + deny_run: None, allow_read: Some(vec![]), + deny_read: None, allow_sys: Some(vec![]), + deny_sys: None, allow_write: Some(vec![]), + deny_write: None, allow_ffi: Some(vec![]), + deny_ffi: None, allow_hrtime: true, ..Flags::default() } @@ -4804,6 +5157,31 @@ mod tests { ); } + #[test] + fn deny_read_denylist() { + use test_util::TempDir; + let temp_dir_guard = TempDir::new(); + let temp_dir = temp_dir_guard.path().to_path_buf(); + + let r = flags_from_vec(svec![ + "deno", + "run", + format!("--deny-read=.,{}", temp_dir.to_str().unwrap()), + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + deny_read: Some(vec![PathBuf::from("."), temp_dir]), + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + ..Flags::default() + } + ); + } + #[test] fn allow_write_allowlist() { use test_util::TempDir; @@ -4829,6 +5207,31 @@ mod tests { ); } + #[test] + fn deny_write_denylist() { + use test_util::TempDir; + let temp_dir_guard = TempDir::new(); + let temp_dir = temp_dir_guard.path().to_path_buf(); + + let r = flags_from_vec(svec![ + "deno", + "run", + format!("--deny-write=.,{}", temp_dir.to_str().unwrap()), + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + deny_write: Some(vec![PathBuf::from("."), temp_dir]), + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + ..Flags::default() + } + ); + } + #[test] fn allow_net_allowlist() { let r = flags_from_vec(svec![ @@ -4850,6 +5253,23 @@ mod tests { ); } + #[test] + fn deny_net_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-net=127.0.0.1", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec!["127.0.0.1"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist() { let r = @@ -4867,6 +5287,23 @@ mod tests { ); } + #[test] + fn deny_env_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_env: Some(svec!["HOME"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist_multiple() { let r = flags_from_vec(svec![ @@ -4888,6 +5325,23 @@ mod tests { ); } + #[test] + fn deny_env_denylist_multiple() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME,PATH", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_env: Some(svec!["HOME", "PATH"]), + ..Flags::default() + } + ); + } + #[test] fn allow_env_allowlist_validator() { let r = @@ -4901,6 +5355,19 @@ mod tests { assert!(r.is_err()); } + #[test] + fn deny_env_denylist_validator() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=HOME", "script.ts"]); + assert!(r.is_ok()); + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=H=ME", "script.ts"]); + assert!(r.is_err()); + let r = + flags_from_vec(svec!["deno", "run", "--deny-env=H\0ME", "script.ts"]); + assert!(r.is_err()); + } + #[test] fn allow_sys() { let r = flags_from_vec(svec!["deno", "run", "--allow-sys", "script.ts"]); @@ -4917,6 +5384,22 @@ mod tests { ); } + #[test] + fn deny_sys() { + let r = flags_from_vec(svec!["deno", "run", "--deny-sys", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(vec![]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist() { let r = @@ -4934,6 +5417,23 @@ mod tests { ); } + #[test] + fn deny_sys_denylist() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-sys=hostname", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(svec!["hostname"]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist_multiple() { let r = flags_from_vec(svec![ @@ -4955,6 +5455,27 @@ mod tests { ); } + #[test] + fn deny_sys_denylist_multiple() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,osRelease", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_sys: Some(svec!["hostname", "osRelease"]), + ..Flags::default() + } + ); + } + #[test] fn allow_sys_allowlist_validator() { let r = @@ -4979,6 +5500,29 @@ mod tests { assert!(r.is_err()); } + #[test] + fn deny_sys_denylist_validator() { + let r = + flags_from_vec(svec!["deno", "run", "--deny-sys=hostname", "script.ts"]); + assert!(r.is_ok()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,osRelease", + "script.ts" + ]); + assert!(r.is_ok()); + let r = flags_from_vec(svec!["deno", "run", "--deny-sys=foo", "script.ts"]); + assert!(r.is_err()); + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-sys=hostname,foo", + "script.ts" + ]); + assert!(r.is_err()); + } + #[test] fn reload_validator() { let r = flags_from_vec(svec![ @@ -5850,6 +6394,35 @@ mod tests { ); } + #[test] + fn deny_net_denylist_with_ports() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-net=deno.land,:8000,:4545", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec![ + "deno.land", + "0.0.0.0:8000", + "127.0.0.1:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "localhost:4545" + ]), + ..Flags::default() + } + ); + } + #[test] fn allow_net_allowlist_with_ipv6_address() { let r = flags_from_vec(svec![ @@ -5882,6 +6455,38 @@ mod tests { ); } + #[test] + fn deny_net_denylist_with_ipv6_address() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--deny-net=deno.land,deno.land:80,::,127.0.0.1,[::1],1.2.3.4:5678,:5678,[::1]:8080", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + watch: None, + script: "script.ts".to_string(), + }), + deny_net: Some(svec![ + "deno.land", + "deno.land:80", + "::", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "localhost:5678", + "[::1]:8080" + ]), + ..Flags::default() + } + ); + } + #[test] fn lock_write() { let r = flags_from_vec(svec![ diff --git a/cli/args/flags_allow_net.rs b/cli/args/flags_allow_net.rs deleted file mode 100644 index 9f8a6b9f9..000000000 --- a/cli/args/flags_allow_net.rs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use deno_core::url::Url; -use std::net::IpAddr; -use std::str::FromStr; - -#[derive(Debug, PartialEq, Eq)] -pub struct ParsePortError(String); - -#[derive(Debug, PartialEq, Eq)] -pub struct BarePort(u16); - -impl FromStr for BarePort { - type Err = ParsePortError; - fn from_str(s: &str) -> Result { - if s.starts_with(':') { - match s.split_at(1).1.parse::() { - Ok(port) => Ok(BarePort(port)), - Err(e) => Err(ParsePortError(e.to_string())), - } - } else { - Err(ParsePortError( - "Bare Port doesn't start with ':'".to_string(), - )) - } - } -} - -pub fn validator(host_and_port: &str) -> Result { - if Url::parse(&format!("internal://{host_and_port}")).is_ok() - || host_and_port.parse::().is_ok() - || host_and_port.parse::().is_ok() - { - Ok(host_and_port.to_string()) - } else { - Err(format!("Bad host:port pair: {host_and_port}")) - } -} - -/// Expands "bare port" paths (eg. ":8080") into full paths with hosts. It -/// expands to such paths into 3 paths with following hosts: `0.0.0.0:port`, -/// `127.0.0.1:port` and `localhost:port`. -pub fn parse(paths: Vec) -> clap::error::Result> { - let mut out: Vec = vec![]; - for host_and_port in paths.iter() { - if Url::parse(&format!("internal://{host_and_port}")).is_ok() - || host_and_port.parse::().is_ok() - { - out.push(host_and_port.to_owned()) - } else if let Ok(port) = host_and_port.parse::() { - // we got bare port, let's add default hosts - for host in ["0.0.0.0", "127.0.0.1", "localhost"].iter() { - out.push(format!("{}:{}", host, port.0)); - } - } else { - return Err(clap::Error::raw( - clap::error::ErrorKind::InvalidValue, - format!("Bad host:port pair: {host_and_port}"), - )); - } - } - Ok(out) -} - -#[cfg(test)] -mod bare_port_tests { - use super::BarePort; - use super::ParsePortError; - - #[test] - fn bare_port_parsed() { - let expected = BarePort(8080); - let actual = ":8080".parse::(); - assert_eq!(actual, Ok(expected)); - } - - #[test] - fn bare_port_parse_error1() { - let expected = - ParsePortError("Bare Port doesn't start with ':'".to_string()); - let actual = "8080".parse::(); - assert_eq!(actual, Err(expected)); - } - - #[test] - fn bare_port_parse_error2() { - let actual = ":65536".parse::(); - assert!(actual.is_err()); - } - - #[test] - fn bare_port_parse_error3() { - let actual = ":14u16".parse::(); - assert!(actual.is_err()); - } - - #[test] - fn bare_port_parse_error4() { - let actual = "Deno".parse::(); - assert!(actual.is_err()); - } - - #[test] - fn bare_port_parse_error5() { - let actual = "deno.land:8080".parse::(); - assert!(actual.is_err()); - } -} - -#[cfg(test)] -mod tests { - use super::parse; - - // Creates vector of strings, Vec - macro_rules! svec { - ($($x:expr),*) => (vec![$($x.to_string()),*]); - } - - #[test] - fn parse_net_args_() { - let entries = svec![ - "deno.land", - "deno.land:80", - "::", - "::1", - "127.0.0.1", - "[::1]", - "1.2.3.4:5678", - "0.0.0.0:5678", - "127.0.0.1:5678", - "[::]:5678", - "[::1]:5678", - "localhost:5678", - "[::1]:8080", - "[::]:8000", - "[::1]:8000", - "localhost:8000", - "0.0.0.0:4545", - "127.0.0.1:4545", - "999.0.88.1:80" - ]; - let expected = svec![ - "deno.land", - "deno.land:80", - "::", - "::1", - "127.0.0.1", - "[::1]", - "1.2.3.4:5678", - "0.0.0.0:5678", - "127.0.0.1:5678", - "[::]:5678", - "[::1]:5678", - "localhost:5678", - "[::1]:8080", - "[::]:8000", - "[::1]:8000", - "localhost:8000", - "0.0.0.0:4545", - "127.0.0.1:4545", - "999.0.88.1:80" - ]; - let actual = parse(entries).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn parse_net_args_expansion() { - let entries = svec![":8080"]; - let expected = svec!["0.0.0.0:8080", "127.0.0.1:8080", "localhost:8080"]; - let actual = parse(entries).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn parse_net_args_ipv6() { - let entries = - svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; - let expected = - svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; - let actual = parse(entries).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn parse_net_args_ipv6_error1() { - let entries = svec![":::"]; - assert!(parse(entries).is_err()); - } - - #[test] - fn parse_net_args_ipv6_error2() { - let entries = svec!["0123:4567:890a:bcde:fg::"]; - assert!(parse(entries).is_err()); - } - - #[test] - fn parse_net_args_ipv6_error3() { - let entries = svec!["[::q]:8080"]; - assert!(parse(entries).is_err()); - } -} diff --git a/cli/args/flags_net.rs b/cli/args/flags_net.rs new file mode 100644 index 000000000..9f8a6b9f9 --- /dev/null +++ b/cli/args/flags_net.rs @@ -0,0 +1,202 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_core::url::Url; +use std::net::IpAddr; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq)] +pub struct ParsePortError(String); + +#[derive(Debug, PartialEq, Eq)] +pub struct BarePort(u16); + +impl FromStr for BarePort { + type Err = ParsePortError; + fn from_str(s: &str) -> Result { + if s.starts_with(':') { + match s.split_at(1).1.parse::() { + Ok(port) => Ok(BarePort(port)), + Err(e) => Err(ParsePortError(e.to_string())), + } + } else { + Err(ParsePortError( + "Bare Port doesn't start with ':'".to_string(), + )) + } + } +} + +pub fn validator(host_and_port: &str) -> Result { + if Url::parse(&format!("internal://{host_and_port}")).is_ok() + || host_and_port.parse::().is_ok() + || host_and_port.parse::().is_ok() + { + Ok(host_and_port.to_string()) + } else { + Err(format!("Bad host:port pair: {host_and_port}")) + } +} + +/// Expands "bare port" paths (eg. ":8080") into full paths with hosts. It +/// expands to such paths into 3 paths with following hosts: `0.0.0.0:port`, +/// `127.0.0.1:port` and `localhost:port`. +pub fn parse(paths: Vec) -> clap::error::Result> { + let mut out: Vec = vec![]; + for host_and_port in paths.iter() { + if Url::parse(&format!("internal://{host_and_port}")).is_ok() + || host_and_port.parse::().is_ok() + { + out.push(host_and_port.to_owned()) + } else if let Ok(port) = host_and_port.parse::() { + // we got bare port, let's add default hosts + for host in ["0.0.0.0", "127.0.0.1", "localhost"].iter() { + out.push(format!("{}:{}", host, port.0)); + } + } else { + return Err(clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + format!("Bad host:port pair: {host_and_port}"), + )); + } + } + Ok(out) +} + +#[cfg(test)] +mod bare_port_tests { + use super::BarePort; + use super::ParsePortError; + + #[test] + fn bare_port_parsed() { + let expected = BarePort(8080); + let actual = ":8080".parse::(); + assert_eq!(actual, Ok(expected)); + } + + #[test] + fn bare_port_parse_error1() { + let expected = + ParsePortError("Bare Port doesn't start with ':'".to_string()); + let actual = "8080".parse::(); + assert_eq!(actual, Err(expected)); + } + + #[test] + fn bare_port_parse_error2() { + let actual = ":65536".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error3() { + let actual = ":14u16".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error4() { + let actual = "Deno".parse::(); + assert!(actual.is_err()); + } + + #[test] + fn bare_port_parse_error5() { + let actual = "deno.land:8080".parse::(); + assert!(actual.is_err()); + } +} + +#[cfg(test)] +mod tests { + use super::parse; + + // Creates vector of strings, Vec + macro_rules! svec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[test] + fn parse_net_args_() { + let entries = svec![ + "deno.land", + "deno.land:80", + "::", + "::1", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "[::]:5678", + "[::1]:5678", + "localhost:5678", + "[::1]:8080", + "[::]:8000", + "[::1]:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "999.0.88.1:80" + ]; + let expected = svec![ + "deno.land", + "deno.land:80", + "::", + "::1", + "127.0.0.1", + "[::1]", + "1.2.3.4:5678", + "0.0.0.0:5678", + "127.0.0.1:5678", + "[::]:5678", + "[::1]:5678", + "localhost:5678", + "[::1]:8080", + "[::]:8000", + "[::1]:8000", + "localhost:8000", + "0.0.0.0:4545", + "127.0.0.1:4545", + "999.0.88.1:80" + ]; + let actual = parse(entries).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn parse_net_args_expansion() { + let entries = svec![":8080"]; + let expected = svec!["0.0.0.0:8080", "127.0.0.1:8080", "localhost:8080"]; + let actual = parse(entries).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn parse_net_args_ipv6() { + let entries = + svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; + let expected = + svec!["::", "::1", "[::1]", "[::]:5678", "[::1]:5678", "::cafe"]; + let actual = parse(entries).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn parse_net_args_ipv6_error1() { + let entries = svec![":::"]; + assert!(parse(entries).is_err()); + } + + #[test] + fn parse_net_args_ipv6_error2() { + let entries = svec!["0123:4567:890a:bcde:fg::"]; + assert!(parse(entries).is_err()); + } + + #[test] + fn parse_net_args_ipv6_error3() { + let entries = svec!["[::q]:8080"]; + assert!(parse(entries).is_err()); + } +} diff --git a/cli/args/mod.rs b/cli/args/mod.rs index cea0c0ca1..7b3b0aa83 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -2,7 +2,7 @@ mod config_file; mod flags; -mod flags_allow_net; +mod flags_net; mod import_map; mod lockfile; pub mod package_json; @@ -1105,13 +1105,21 @@ impl CliOptions { pub fn permissions_options(&self) -> PermissionsOptions { PermissionsOptions { allow_env: self.flags.allow_env.clone(), + deny_env: self.flags.deny_env.clone(), allow_hrtime: self.flags.allow_hrtime, + deny_hrtime: self.flags.deny_hrtime, allow_net: self.flags.allow_net.clone(), + deny_net: self.flags.deny_net.clone(), allow_ffi: self.flags.allow_ffi.clone(), + deny_ffi: self.flags.deny_ffi.clone(), allow_read: self.flags.allow_read.clone(), + deny_read: self.flags.deny_read.clone(), allow_run: self.flags.allow_run.clone(), + deny_run: self.flags.deny_run.clone(), allow_sys: self.flags.allow_sys.clone(), + deny_sys: self.flags.deny_sys.clone(), allow_write: self.flags.allow_write.clone(), + deny_write: self.flags.deny_write.clone(), prompt: !self.no_prompt(), } } -- cgit v1.2.3