Skip to content

Commit

Permalink
fix(permissions): implicit --allow-import when using --cached-only (
Browse files Browse the repository at this point in the history
#27530)

`--cached-only` cannot communicate with a remote server

Closes #27498
  • Loading branch information
dsherret authored Jan 2, 2025
1 parent 0457c38 commit 2092f0c
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 146 deletions.
120 changes: 0 additions & 120 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::borrow::Cow;
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
Expand Down Expand Up @@ -34,7 +33,6 @@ use deno_core::url::Url;
use deno_graph::GraphKind;
use deno_path_util::normalize_path;
use deno_path_util::url_to_file_path;
use deno_runtime::deno_permissions::PermissionsOptions;
use deno_runtime::deno_permissions::SysDescriptor;
use deno_telemetry::OtelConfig;
use deno_telemetry::OtelConsoleConfig;
Expand All @@ -44,8 +42,6 @@ use serde::Deserialize;
use serde::Serialize;

use super::flags_net;
use super::jsr_url;
use crate::args::resolve_no_prompt;
use crate::util::fs::canonicalize_path;

#[derive(Clone, Debug, Default, Eq, PartialEq)]
Expand Down Expand Up @@ -692,97 +688,6 @@ impl PermissionFlags {
|| self.deny_write.is_some()
|| self.allow_import.is_some()
}

pub fn to_options(&self, cli_arg_urls: &[Cow<Url>]) -> PermissionsOptions {
fn handle_allow<T: Default>(
allow_all: bool,
value: Option<T>,
) -> Option<T> {
if allow_all {
assert!(value.is_none());
Some(T::default())
} else {
value
}
}

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 = [
"jsr.io:443",
"deno.land:443",
"esm.sh:443",
"cdn.jsdelivr.net: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()),
deny_env: self.deny_env.clone(),
allow_net: handle_allow(self.allow_all, self.allow_net.clone()),
deny_net: self.deny_net.clone(),
allow_ffi: handle_allow(self.allow_all, self.allow_ffi.clone()),
deny_ffi: self.deny_ffi.clone(),
allow_read: handle_allow(self.allow_all, self.allow_read.clone()),
deny_read: self.deny_read.clone(),
allow_run: handle_allow(self.allow_all, self.allow_run.clone()),
deny_run: self.deny_run.clone(),
allow_sys: handle_allow(self.allow_all, self.allow_sys.clone()),
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 {
Expand Down Expand Up @@ -11549,8 +11454,6 @@ mod tests {
..Default::default()
}
);
// just make sure this doesn't panic
let _ = flags.permissions.to_options(&[]);
}

#[test]
Expand Down Expand Up @@ -11626,29 +11529,6 @@ 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);
}

#[test]
fn allow_all_conflicts_allow_perms() {
let flags = [
Expand Down
146 changes: 131 additions & 15 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1526,20 +1526,100 @@ impl CliOptions {
self.flags.no_npm
}

pub fn permission_flags(&self) -> &PermissionFlags {
&self.flags.permissions
pub fn permissions_options(&self) -> PermissionsOptions {
// bury this in here to ensure people use cli_options.permissions_options()
fn flags_to_options(flags: &PermissionFlags) -> PermissionsOptions {
fn handle_allow<T: Default>(
allow_all: bool,
value: Option<T>,
) -> Option<T> {
if allow_all {
assert!(value.is_none());
Some(T::default())
} else {
value
}
}

PermissionsOptions {
allow_all: flags.allow_all,
allow_env: handle_allow(flags.allow_all, flags.allow_env.clone()),
deny_env: flags.deny_env.clone(),
allow_net: handle_allow(flags.allow_all, flags.allow_net.clone()),
deny_net: flags.deny_net.clone(),
allow_ffi: handle_allow(flags.allow_all, flags.allow_ffi.clone()),
deny_ffi: flags.deny_ffi.clone(),
allow_read: handle_allow(flags.allow_all, flags.allow_read.clone()),
deny_read: flags.deny_read.clone(),
allow_run: handle_allow(flags.allow_all, flags.allow_run.clone()),
deny_run: flags.deny_run.clone(),
allow_sys: handle_allow(flags.allow_all, flags.allow_sys.clone()),
deny_sys: flags.deny_sys.clone(),
allow_write: handle_allow(flags.allow_all, flags.allow_write.clone()),
deny_write: flags.deny_write.clone(),
allow_import: handle_allow(flags.allow_all, flags.allow_import.clone()),
prompt: !resolve_no_prompt(flags),
}
}

let mut permissions_options = flags_to_options(&self.flags.permissions);
self.augment_import_permissions(&mut permissions_options);
permissions_options
}

pub fn permissions_options(&self) -> PermissionsOptions {
fn augment_import_permissions(&self, options: &mut PermissionsOptions) {
// do not add if the user specified --allow-all or --allow-import
if !options.allow_all && options.allow_import.is_none() {
options.allow_import = Some(self.implicit_allow_import());
}
}

fn implicit_allow_import(&self) -> Vec<String> {
// allow importing from anywhere when using cached only
if self.cache_setting() == CacheSetting::Only {
vec![] // allow all imports
} else {
// implicitly allow some trusted hosts and the CLI arg urls
let cli_arg_urls = self.get_cli_arg_urls();
let builtin_allowed_import_hosts = [
"jsr.io:443",
"deno.land:443",
"esm.sh:443",
"cdn.jsdelivr.net:443",
"raw.githubusercontent.com:443",
"gist.githubusercontent.com:443",
];
let mut imports = Vec::with_capacity(
builtin_allowed_import_hosts.len() + cli_arg_urls.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()) {
if jsr_host != "jsr.io:443" {
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);
}
}
imports
}
}

fn get_cli_arg_urls(&self) -> Vec<Cow<'_, Url>> {
fn files_to_urls(files: &[String]) -> Vec<Cow<'_, Url>> {
files
.iter()
.filter_map(|f| Url::parse(f).ok().map(Cow::Owned))
.collect()
files.iter().filter_map(|f| file_to_url(f)).collect()
}

// get a list of urls to imply for --allow-import
let cli_arg_urls = self
fn file_to_url(file: &str) -> Option<Cow<'_, Url>> {
Url::parse(file).ok().map(Cow::Owned)
}

self
.resolve_main_module()
.ok()
.map(|url| vec![Cow::Borrowed(url)])
Expand All @@ -1551,18 +1631,18 @@ impl CliOptions {
Some(files_to_urls(&check_flags.files))
}
DenoSubcommand::Install(InstallFlags::Global(flags)) => {
Url::parse(&flags.module_url)
.ok()
.map(|url| vec![Cow::Owned(url)])
file_to_url(&flags.module_url).map(|url| vec![url])
}
DenoSubcommand::Doc(DocFlags {
source_files: DocSourceFileFlag::Paths(paths),
..
}) => Some(files_to_urls(paths)),
DenoSubcommand::Info(InfoFlags {
file: Some(file), ..
}) => file_to_url(file).map(|url| vec![url]),
_ => None,
})
.unwrap_or_default();
self.flags.permissions.to_options(&cli_arg_urls)
.unwrap_or_default()
}

pub fn reload_flag(&self) -> bool {
Expand Down Expand Up @@ -1998,14 +2078,28 @@ fn load_env_variables_from_env_file(filename: Option<&Vec<String>>) {
}
}

/// 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 {
match url.scheme() {
"https" => Some(format!("{}:443", host)),
"http" => Some(format!("{}:80", host)),
_ => None,
}
}
}

#[derive(Debug, Clone, Copy)]
pub enum NpmCachingStrategy {
Eager,
Lazy,
Manual,
}

pub(crate) fn otel_runtime_config() -> OtelRuntimeConfig {
pub fn otel_runtime_config() -> OtelRuntimeConfig {
OtelRuntimeConfig {
runtime_name: Cow::Borrowed("deno"),
runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno),
Expand Down Expand Up @@ -2102,4 +2196,26 @@ mod test {
let reg_api_url = jsr_api_url();
assert!(reg_api_url.as_str().ends_with('/'));
}

#[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("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);
}
}
5 changes: 3 additions & 2 deletions cli/standalone/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use deno_runtime::deno_fs::FileSystem;
use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_io::fs::FsError;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::deno_permissions::PermissionsOptions;
use deno_semver::npm::NpmVersionReqParseError;
use deno_semver::package::PackageReq;
use deno_semver::Version;
Expand Down Expand Up @@ -188,7 +189,7 @@ pub struct Metadata {
pub argv: Vec<String>,
pub seed: Option<u64>,
pub code_cache_key: Option<u64>,
pub permissions: PermissionFlags,
pub permissions: PermissionsOptions,
pub location: Option<Url>,
pub v8_flags: Vec<String>,
pub log_level: Option<Level>,
Expand Down Expand Up @@ -793,7 +794,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
seed: self.cli_options.seed(),
code_cache_key,
location: self.cli_options.location_flag().clone(),
permissions: self.cli_options.permission_flags().clone(),
permissions: self.cli_options.permissions_options(),
v8_flags: self.cli_options.v8_flags().clone(),
unsafely_ignore_certificate_errors: self
.cli_options
Expand Down
3 changes: 1 addition & 2 deletions cli/standalone/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,8 +920,7 @@ pub async fn run(
};

let permissions = {
let mut permissions =
metadata.permissions.to_options(/* cli_arg_urls */ &[]);
let mut permissions = metadata.permissions;
// grant read access to the vfs
match &mut permissions.allow_read {
Some(vec) if vec.is_empty() => {
Expand Down
Loading

0 comments on commit 2092f0c

Please sign in to comment.