CVE-2024-27936: Insufficient Information (NVD-CWE-noinfo)
value: Deno is a JavaScript, TypeScript, and WebAssembly runtime with secure defaults. Starting in version 1.32.1 and prior to version 1.41.0 of the deno library, maliciously crafted permission request can show the spoofed permission prompt by inserting a broken ANSI escape sequence into the request contents. Deno is stripping any ANSI escape sequences from the permission prompt, but permissions given to the program are based on the contents that contain the ANSI escape sequences. Any Deno program can spoof the content of the interactive permission prompt by inserting a broken ANSI code, which allows a malicious Deno program to display the wrong file path or program name to the user. Version 1.41.0 of the deno library contains a patch for the issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-21T02:52Z
Repository: https://github.com/denoland/deno
Language: Other
Files changed: Cargo.lock, Cargo.toml, cli/Cargo.toml, cli/tests/integration/run_tests.rs, runtime/Cargo.toml, runtime/permissions/prompter.rs
Commit date: 2023-03-10 01:09:14 +0100
Commit message: fix(prompt): better output with control chars (#18108)


Diff: diff --git a/Cargo.lock b/Cargo.lock
index 3233b03c9..97d27d34c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -523,13 +523,13 @@ dependencies = [
  "unicode-width",
 ]
 
 [[package]]
 name = "console_static_text"
-version = "0.3.4"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f166cdfb9db0607e2079b382ba64bc4164344006c733b95c1ecfa782a180a34a"
+checksum = "953d2c3cf53213a4eccdbe8f2e0b49b5d0f77e87a2a9060117bbf9346f92b64e"
 dependencies = [
  "unicode-width",
  "vte",
 ]
 
@@ -1283,10 +1283,11 @@ dependencies = [
 [[package]]
 name = "deno_runtime"
 version = "0.99.0"
 dependencies = [
  "atty",
+ "console_static_text",
  "deno_ast",
  "deno_broadcast_channel",
  "deno_cache",
  "deno_console",
  "deno_core",
diff --git a/Cargo.toml b/Cargo.toml
index da026daf1..2e9d34b85 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -82,10 +82,11 @@ async-trait = "0.1.51"
 atty = "=0.2.14"
 base64 = "=0.13.1"
 bencher = "0.1"
 bytes = "=1.2.1"
 cache_control = "=0.2.0"
+console_static_text = "=0.7.1"
 data-url = "=0.2.0"
 dlopen = "0.1.8"
 encoding_rs = "=0.8.31"
 flate2 = "=1.0.24"
 fs3 = "0.5.0"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 65e4359c2..27409ec91 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -60,11 +60,11 @@ base64.workspace = true
 cache_control.workspace = true
 chrono = { version = "=0.4.22", default-features = false, features = ["clock"] }
 clap = "=3.1.12"
 clap_complete = "=3.1.2"
 clap_complete_fig = "=3.1.5"
-console_static_text = "=0.3.4"
+console_static_text.workspace = true
 data-url.workspace = true
 dissimilar = "=1.0.4"
 dprint-plugin-json = "=0.17.0"
 dprint-plugin-markdown = "=0.15.2"
 dprint-plugin-typescript = "=0.83.0"
diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs
index 239311975..c3a2c45a7 100644
--- a/cli/tests/integration/run_tests.rs
+++ b/cli/tests/integration/run_tests.rs
@@ -4025,10 +4025,55 @@ fn stdio_streams_are_locked_in_permission_prompt() {
     let expected_output = r#"\x1b[1;1H\x1b[0JAre you sure you want to continue?"#;
     assert_eq!(output, expected_output);
   });
 }
 
+#[test]
+fn permission_prompt_strips_ansi_codes_and_control_chars() {
+  let _guard = util::http_server();
+  util::with_pty(&["repl"], |mut console| {
+    console.write_line(
+      r#"Deno.permissions.request({ name: "env", variable: "\rDo you like ice cream? y/n" });"#
+    );
+    console.write_line("close();");
+    let output = console.read_all_output();
+
+    assert!(output.contains(
+      "┌ ⚠️  Deno requests env access to \"Do you like ice cream? y/n\"."
+    ));
+  });
+
+  util::with_pty(&["repl"], |mut console| {
+    console.write_line(
+      r#"
+const boldANSI = "\u001b[1m" // bold
+const unboldANSI = "\u001b[22m" // unbold
+
+const prompt = `┌ ⚠️  ${boldANSI}Deno requests run access to "echo"${unboldANSI}
+├ Requested by \`Deno.Command().output()`
+
+const moveANSIUp = "\u001b[1A" // moves to the start of the line
+const clearANSI = "\u001b[2K" // clears the line
+const moveANSIStart = "\u001b[1000D" // moves to the start of the line
+
+Deno[Object.getOwnPropertySymbols(Deno)[0]].core.ops.op_spawn_child({
+    cmd: "cat",
+    args: ["/etc/passwd"],
+    clearEnv: false,
+    env: [],
+    stdin: "null",
+    stdout: "inherit",
+    stderr: "piped"
+}, moveANSIUp + clearANSI + moveANSIStart + prompt)"#,
+    );
+    console.write_line("close();");
+    let output = console.read_all_output();
+
+    assert!(output.contains(r#"┌ ⚠️  Deno requests run access to "cat""#));
+  });
+}
+
 itest!(node_builtin_modules_ts {
   args: "run --quiet --allow-read run/node_builtin_modules/mod.ts hello there",
   output: "run/node_builtin_modules/mod.ts.out",
   envs: env_vars_for_npm_tests_no_sync_download(),
   exit_code: 0,
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 0ba8f8d3a..360ef2aa0 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -84,10 +84,11 @@ deno_webgpu.workspace = true
 deno_webidl.workspace = true
 deno_websocket.workspace = true
 deno_webstorage.workspace = true
 
 atty.workspace = true
+console_static_text.workspace = true
 dlopen.workspace = true
 encoding_rs.workspace = true
 filetime = "0.2.16"
 fs3.workspace = true
 http.workspace = true
diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs
index d148b485e..d94264b1a 100644
--- a/runtime/permissions/prompter.rs
+++ b/runtime/permissions/prompter.rs
@@ -3,10 +3,18 @@
 use crate::colors;
 use deno_core::error::AnyError;
 use deno_core::parking_lot::Mutex;
 use once_cell::sync::Lazy;
 
+/// Helper function to strip ansi codes and ASCII control characters.
+fn strip_ansi_codes_and_ascii_control(s: &str) -> std::borrow::Cow<str> {
+  console_static_text::strip_ansi_codes(s)
+    .chars()
+    .filter(|c| !c.is_ascii_control())
+    .collect()
+}
+
 pub const PERMISSION_EMOJI: &str = "⚠️";
 
 #[derive(Debug, Eq, PartialEq)]
 pub enum PromptResponse {
   Allow,
@@ -201,21 +209,25 @@ impl PermissionPrompter for TtyPrompter {
     // Lock stdio streams, so no other output is written while the prompt is
     // displayed.
     let _stdout_guard = std::io::stdout().lock();
     let _stderr_guard = std::io::stderr().lock();
 
+    let message = strip_ansi_codes_and_ascii_control(message);
+    let name = strip_ansi_codes_and_ascii_control(name);
+    let api_name = api_name.map(strip_ansi_codes_and_ascii_control);
+
     // print to stderr so that if stdout is piped this is still displayed.
     let opts: String = if is_unary {
       format!("[y/n/A] (y = yes, allow; n = no, deny; A = allow all {name} permissions)")
     } else {
       "[y/n] (y = yes, allow; n = no, deny)".to_string()
     };
     eprint!("┌ {PERMISSION_EMOJI}  ");
     eprint!("{}", colors::bold("Deno requests "));
-    eprint!("{}", colors::bold(message));
+    eprint!("{}", colors::bold(message.clone()));
     eprintln!("{}", colors::bold("."));
-    if let Some(api_name) = api_name {
+    if let Some(api_name) = api_name.clone() {
       eprintln!("├ Requested by `{api_name}` API");
     }
     let msg = format!("Run again with --allow-{name} to bypass this prompt.");
     eprintln!("├ {}", colors::italic(&msg));
     eprint!("└ {}", colors::bold("Allow?"));

Vulnerable code: version = "0.3.4"
checksum = "f166cdfb9db0607e2079b382ba64bc4164344006c733b95c1ecfa782a180a34a"
console_static_text = "=0.3.4"
    eprint!("{}", colors::bold(message));
    if let Some(api_name) = api_name {
Fixed code: version = "0.7.1"
checksum = "953d2c3cf53213a4eccdbe8f2e0b49b5d0f77e87a2a9060117bbf9346f92b64e"
 "console_static_text",
console_static_text = "=0.7.1"
console_static_text.workspace = true
#[test]
fn permission_prompt_strips_ansi_codes_and_control_chars() {
  let _guard = util::http_server();
  util::with_pty(&["repl"], |mut console| {
    console.write_line(
      r#"Deno.permissions.request({ name: "env", variable: "\rDo you like ice cream? y/n" });"#
    );
    console.write_line("close();");
    let output = console.read_all_output();

    assert!(output.contains(
      "┌ ⚠️  Deno requests env access to \"Do you like ice cream? y/n\"."
    ));
  });

  util::with_pty(&["repl"], |mut console| {
    console.write_line(
      r#"
const boldANSI = "\u001b[1m" // bold
const unboldANSI = "\u001b[22m" // unbold

const prompt = `┌ ⚠️  ${boldANSI}Deno requests run access to "echo"${unboldANSI}
├ Requested by \`Deno.Command().output()`

const moveANSIUp = "\u001b[1A" // moves to the start of the line
const clearANSI = "\u001b[2K" // clears the line
const moveANSIStart = "\u001b[1000D" // moves to the start of the line

Deno[Object.getOwnPropertySymbols(Deno)[0]].core.ops.op_spawn_child({
    cmd: "cat",
    args: ["/etc/passwd"],
    clearEnv: false,
    env: [],
    stdin: "null",
    stdout: "inherit",
    stderr: "piped"
}, moveANSIUp + clearANSI + moveANSIStart + prompt)"#,
    );
    console.write_line("close();");
    let output = console.read_all_output();

    assert!(output.contains(r#"┌ ⚠️  Deno requests run access to "cat""#));
  });
}

console_static_text.workspace = true
/// Helper function to strip ansi codes and ASCII control characters.
fn strip_ansi_codes_and_ascii_control(s: &str) -> std::borrow::Cow<str> {
  console_static_text::strip_ansi_codes(s)
    .chars()
    .filter(|c| !c.is_ascii_control())
    .collect()
}

    let message = strip_ansi_codes_and_ascii_control(message);
    let name = strip_ansi_codes_and_ascii_control(name);
    let api_name = api_name.map(strip_ansi_codes_and_ascii_control);

    eprint!("{}", colors::bold(message.clone()));
    if let Some(api_name) = api_name.clone() {