CVE-2024-27936 (published 2024-03-21T02:52Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/denoland/deno
commit_details: repo_url: https://github.com/denoland/deno
author: Bartek Iwańczuk
author_date: 2023-03-09 20:09:14-04:00
msg: fix(prompt): better output with control chars (#18108)
repository: repo_url: https://github.com/denoland/deno
repo_name: denoland/deno
description: A modern runtime for JavaScript and TypeScript.
repo_language: Rust
owner: denoland
file_changes: filename: Cargo.lock
change_type: ModificationType.MODIFY
diff: @@ -525,9 +525,9 @@ dependencies = [
 
 [[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",
@@ -1285,6 +1285,7 @@ name = "deno_runtime"
 version = "0.99.0"
 dependencies = [
  "atty",
+ "console_static_text",
  "deno_ast",
  "deno_broadcast_channel",
  "deno_cache",

programming_language: PowerShell
filename: Cargo.toml
change_type: ModificationType.MODIFY
diff: @@ -84,6 +84,7 @@ 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"

programming_language: Lua
filename: Cargo.toml
change_type: ModificationType.MODIFY
diff: @@ -62,7 +62,7 @@ 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"

programming_language: Lua
filename: run_tests.rs
change_type: ModificationType.MODIFY
diff: @@ -4027,6 +4027,51 @@ fn stdio_streams_are_locked_in_permission_prompt() {
   });
 }
 
+#[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",

programming_language: Rust
filename: Cargo.toml
change_type: ModificationType.MODIFY
diff: @@ -86,6 +86,7 @@ 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"

programming_language: Lua
filename: prompter.rs
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,14 @@ 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)]
@@ -203,6 +211,10 @@ impl PermissionPrompter for TtyPrompter {
     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)")
@@ -211,9 +223,9 @@ impl PermissionPrompter for TtyPrompter {
     };
     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.");

programming_language: Rust
repo_url: https://github.com/denoland/deno
commit_details: repo_url: https://github.com/denoland/deno
author: Matt Mastracci
author_date: 2024-02-18 21:51:06-07:00
msg: 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.
repository: repo_url: https://github.com/denoland/deno
repo_name: denoland/deno
description: A modern runtime for JavaScript and TypeScript.
repo_language: Rust
owner: denoland
file_changes: filename: prompter.rs
change_type: ModificationType.MODIFY
diff: @@ -11,12 +11,24 @@ 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 = "⚠️";
@@ -249,9 +261,9 @@ impl PermissionPrompter for TtyPrompter {
       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 {

programming_language: Rust
filename: run_tests.rs
change_type: ModificationType.MODIFY
diff: @@ -4725,19 +4725,19 @@ 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
     ))
   });
@@ -4747,14 +4747,10 @@ fn permission_prompt_strips_ansi_codes_and_control_chars() {
     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\".");
   });
 }
 

programming_language: JavaScript