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: Rust
Files changed: runtime/permissions/prompter.rs, tests/integration/run_tests.rs
Commit date: 2024-02-19 04:51:06 +0000
Commit message: feat(core): highlight unprintable chars in permission prompts (#22468)

If we strip out unprintable chars, we don't see the full filename being
requested by permission prompts. Instead, we highlight and escape them
to make them visible.
Diff: diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs
index f054b31f38..da5979f0a2 100644
--- a/runtime/permissions/prompter.rs
+++ b/runtime/permissions/prompter.rs
@@ -9,16 +9,28 @@ use std::io::BufRead;
 use std::io::IsTerminal;
 use std::io::StderrLock;
 use std::io::StdinLock;
 use std::io::Write as IoWrite;
 
-/// 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::ansi::strip_ansi_codes(s)
-    .chars()
-    .filter(|c| !c.is_ascii_control())
-    .collect()
+/// Helper function to make control characters visible so users can see the underlying filename.
+fn escape_control_characters(s: &str) -> std::borrow::Cow<str> {
+  if !s.contains(|c: char| c.is_ascii_control() || c.is_control()) {
+    return std::borrow::Cow::Borrowed(s);
+  }
+  let mut output = String::with_capacity(s.len() * 2);
+  for c in s.chars() {
+    match c {
+      c if c.is_ascii_control() => output.push_str(
+        &colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
+      ),
+      c if c.is_control() => output.push_str(
+        &colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
+      ),
+      c => output.push(c),
+    }
+  }
+  output.into()
 }
 
 pub const PERMISSION_EMOJI: &str = "⚠️";
 
 // 10kB of permission prompting should be enough for anyone
@@ -247,13 +259,13 @@ impl PermissionPrompter for TtyPrompter {
     if let Err(err) = clear_stdin(&mut stdin_lock, &mut stderr_lock) {
       eprintln!("Error clearing stdin for permission prompt. {err:#}");
       return PromptResponse::Deny; // don't grant permission if this fails
     }
 
-    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);
+    let message = escape_control_characters(message);
+    let name = escape_control_characters(name);
+    let api_name = api_name.map(escape_control_characters);
 
     // 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 {
diff --git a/tests/integration/run_tests.rs b/tests/integration/run_tests.rs
index 02fb915b5c..a4bfc6345c 100644
--- a/tests/integration/run_tests.rs
+++ b/tests/integration/run_tests.rs
@@ -4723,40 +4723,36 @@ fn stdio_streams_are_locked_in_permission_prompt() {
       });
   }
 }
 
 #[test]
-fn permission_prompt_strips_ansi_codes_and_control_chars() {
+fn permission_prompt_escapes_ansi_codes_and_control_chars() {
   util::with_pty(&["repl"], |mut console| {
     console.write_line(
         r#"Deno.permissions.request({ name: "env", variable: "\rDo you like ice cream? y/n" });"#
       );
     // will be uppercase on windows
     let env_name = if cfg!(windows) {
-      "DO YOU LIKE ICE CREAM? Y/N"
+      "\\rDO YOU LIKE ICE CREAM? Y/N"
     } else {
-      "Do you like ice cream? y/n"
+      "\\rDo you like ice cream? y/n"
     };
     console.expect(format!(
-      "┌ ⚠️  Deno requests env access to \"{}\".",
+      "\u{250c} \u{26a0}\u{fe0f}  Deno requests env access to \"{}\".",
       env_name
     ))
   });
 
   util::with_pty(&["repl"], |mut console| {
     console.write_line_raw(r#"const boldANSI = "\u001b[1m";"#);
     console.expect("undefined");
     console.write_line_raw(r#"const unboldANSI = "\u001b[22m";"#);
     console.expect("undefined");
-    console.write_line_raw(r#"const prompt = `┌ ⚠️  ${boldANSI}Deno requests run access to "echo"${unboldANSI}\n ├ Requested by \`Deno.Command().output()`"#);
-    console.expect("undefined");
-    console.write_line_raw(r#"const moveANSIUp = "\u001b[1A";"#);
-    console.expect("undefined");
-    console.write_line_raw(r#"const clearANSI = "\u001b[2K";"#);
-    console.expect("undefined");
-    console.write_line_raw(r#"const moveANSIStart = "\u001b[1000D";"#);
-    console.expect("undefined");
+    console.write_line_raw(
+      r#"new Deno.Command(`${boldANSI}cat${unboldANSI}`).spawn();"#,
+    );
+    console.expect("\u{250c} \u{26a0}\u{fe0f}  Deno requests run access to \"\\u{1b}[1mcat\\u{1b}[22m\".");
   });
 }
 
 itest!(node_builtin_modules_ts {
   args: "run --quiet --allow-read run/node_builtin_modules/mod.ts hello there",

Vulnerable code: /// 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::ansi::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);
fn permission_prompt_strips_ansi_codes_and_control_chars() {
      "DO YOU LIKE ICE CREAM? Y/N"
      "Do you like ice cream? y/n"
      "┌ ⚠️  Deno requests env access to \"{}\".",
    console.write_line_raw(r#"const prompt = `┌ ⚠️  ${boldANSI}Deno requests run access to "echo"${unboldANSI}\n ├ Requested by \`Deno.Command().output()`"#);
    console.expect("undefined");
    console.write_line_raw(r#"const moveANSIUp = "\u001b[1A";"#);
    console.expect("undefined");
    console.write_line_raw(r#"const clearANSI = "\u001b[2K";"#);
    console.expect("undefined");
    console.write_line_raw(r#"const moveANSIStart = "\u001b[1000D";"#);
    console.expect("undefined");
Fixed code: /// Helper function to make control characters visible so users can see the underlying filename.
fn escape_control_characters(s: &str) -> std::borrow::Cow<str> {
  if !s.contains(|c: char| c.is_ascii_control() || c.is_control()) {
    return std::borrow::Cow::Borrowed(s);
  }
  let mut output = String::with_capacity(s.len() * 2);
  for c in s.chars() {
    match c {
      c if c.is_ascii_control() => output.push_str(
        &colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
      ),
      c if c.is_control() => output.push_str(
        &colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
      ),
      c => output.push(c),
    }
  }
  output.into()
    let message = escape_control_characters(message);
    let name = escape_control_characters(name);
    let api_name = api_name.map(escape_control_characters);
fn permission_prompt_escapes_ansi_codes_and_control_chars() {
      "\\rDO YOU LIKE ICE CREAM? Y/N"
      "\\rDo you like ice cream? y/n"
      "\u{250c} \u{26a0}\u{fe0f}  Deno requests env access to \"{}\".",
    console.write_line_raw(
      r#"new Deno.Command(`${boldANSI}cat${unboldANSI}`).spawn();"#,
    );
    console.expect("\u{250c} \u{26a0}\u{fe0f}  Deno requests run access to \"\\u{1b}[1mcat\\u{1b}[22m\".");