summaryrefslogtreecommitdiff
path: root/cli/args/flags.rs
diff options
context:
space:
mode:
authorBartek Iwańczuk <biwanczuk@gmail.com>2024-09-26 02:50:54 +0100
committerGitHub <noreply@github.com>2024-09-26 01:50:54 +0000
commit5504acea6751480f1425c88353ad5d36257bdce7 (patch)
treefa02e6c546eae469aac894bfc71600ab4eccad28 /cli/args/flags.rs
parent05415bb9de475aa8646985a545f30fe93136207e (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.rs253
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);
+ }
}