Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(permissions): implicit --allow-import when using --cached-only #27530

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading