mirror of
https://github.com/sharkdp/bat
synced 2026-06-09 10:03:18 +00:00
santize filenames
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
|
||||
|
||||
## Bugfixes
|
||||
- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit)
|
||||
- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar)
|
||||
- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu)
|
||||
- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn)
|
||||
|
||||
+11
-5
@@ -158,7 +158,9 @@ impl HighlightingAssets {
|
||||
let syntax_match = mapping.get_syntax_for(path);
|
||||
|
||||
if let Some(MappingTarget::MapToUnknown) = syntax_match {
|
||||
return Err(Error::UndetectedSyntax(path.to_string_lossy().into()));
|
||||
return Err(Error::UndetectedSyntax(
|
||||
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match {
|
||||
@@ -175,13 +177,17 @@ impl HighlightingAssets {
|
||||
) {
|
||||
(Some(syntax), _) => Ok(syntax),
|
||||
|
||||
(_, Some(MappingTarget::MapExtensionToUnknown)) => {
|
||||
Err(Error::UndetectedSyntax(path.to_string_lossy().into()))
|
||||
}
|
||||
(_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax(
|
||||
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
|
||||
)),
|
||||
|
||||
_ => self
|
||||
.get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)?
|
||||
.ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())),
|
||||
.ok_or_else(|| {
|
||||
Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal(
|
||||
&path.to_string_lossy(),
|
||||
))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -259,7 +259,9 @@ pub fn list_themes(
|
||||
fn set_terminal_title_to(new_terminal_title: String) {
|
||||
let osc_command_for_setting_terminal_title = "\x1b]0;";
|
||||
let osc_end_command = "\x07";
|
||||
print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}");
|
||||
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC.
|
||||
let safe_title = bat::sanitize_for_terminal(&new_terminal_title);
|
||||
print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}");
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -216,20 +216,20 @@ impl<'a> Input<'a> {
|
||||
description,
|
||||
metadata: self.metadata,
|
||||
reader: {
|
||||
let mut file = File::open(&path)
|
||||
.map_err(|e| format!("'{}': {e}", path.to_string_lossy()))?;
|
||||
let path_display =
|
||||
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy());
|
||||
let mut file =
|
||||
File::open(&path).map_err(|e| format!("'{path_display}': {e}"))?;
|
||||
if file.metadata()?.is_dir() {
|
||||
return Err(format!("'{}' is a directory.", path.to_string_lossy()).into());
|
||||
return Err(format!("'{path_display}' is a directory.").into());
|
||||
}
|
||||
|
||||
if let Some(stdout) = stdout_identifier {
|
||||
let input_identifier = Identifier::try_from(file).map_err(|e| {
|
||||
format!("{}: Error identifying file: {e}", path.to_string_lossy())
|
||||
})?;
|
||||
let input_identifier = Identifier::try_from(file)
|
||||
.map_err(|e| format!("{path_display}: Error identifying file: {e}"))?;
|
||||
if stdout.surely_conflicts_with(&input_identifier) {
|
||||
return Err(format!(
|
||||
"IO circle detected. The input from '{}' is also an output. Aborting to avoid infinite loop.",
|
||||
path.to_string_lossy()
|
||||
"IO circle detected. The input from '{path_display}' is also an output. Aborting to avoid infinite loop.",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ mod vscreen;
|
||||
pub(crate) mod wrapping;
|
||||
|
||||
pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation};
|
||||
pub use preprocessor::sanitize_for_terminal;
|
||||
pub use preprocessor::StripAnsiMode;
|
||||
pub use pretty_printer::{Input, PrettyPrinter, Syntax};
|
||||
pub use syntax_mapping::{MappingTarget, SyntaxMapping};
|
||||
|
||||
@@ -149,6 +149,35 @@ pub fn strip_ansi(line: &str) -> String {
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Escape C0, DEL, and C1 control characters so a string from an untrusted
|
||||
/// filename or path can be safely written to the terminal.
|
||||
pub fn sanitize_for_terminal(input: &str) -> String {
|
||||
if !input
|
||||
.chars()
|
||||
.any(|c| matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F'..='\u{9F}'))
|
||||
{
|
||||
return input.to_owned();
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(input.len() + 8);
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\t' => out.push('\t'),
|
||||
'\x00'..='\x1F' => {
|
||||
out.push('^');
|
||||
out.push(char::from_u32(0x40 + c as u32).unwrap_or('?'));
|
||||
}
|
||||
'\x7F' => out.push_str("^?"),
|
||||
'\u{80}'..='\u{9F}' => {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(out, "\\u{{{:x}}}", c as u32);
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Strips overstrike sequences (backspace formatting) from input.
|
||||
///
|
||||
/// Overstrike formatting is used by man pages and some help output:
|
||||
@@ -261,3 +290,40 @@ fn test_strip_overstrike() {
|
||||
// Unicode with overstrike
|
||||
assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_for_terminal_passthrough() {
|
||||
assert_eq!(sanitize_for_terminal(""), "");
|
||||
assert_eq!(sanitize_for_terminal("hello.txt"), "hello.txt");
|
||||
assert_eq!(sanitize_for_terminal("résumé.pdf"), "résumé.pdf");
|
||||
assert_eq!(sanitize_for_terminal("日本語.md"), "日本語.md");
|
||||
assert_eq!(
|
||||
sanitize_for_terminal("path/with spaces/file.log"),
|
||||
"path/with spaces/file.log"
|
||||
);
|
||||
assert_eq!(sanitize_for_terminal("a\tb"), "a\tb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_for_terminal_c0_controls() {
|
||||
assert_eq!(
|
||||
sanitize_for_terminal("\x1b[31mINJECTED\x1b[0m.txt"),
|
||||
"^[[31mINJECTED^[[0m.txt"
|
||||
);
|
||||
assert_eq!(sanitize_for_terminal("bad\x07rest"), "bad^Grest");
|
||||
assert_eq!(sanitize_for_terminal("\x00\x08\n\r\x7F"), "^@^H^J^M^?");
|
||||
assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\u{9b}31m");
|
||||
assert_eq!(
|
||||
sanitize_for_terminal("\u{9d}0;pwned\x07"),
|
||||
"\\u{9d}0;pwned^G"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_for_terminal_idempotent_on_sanitized() {
|
||||
let dirty = "\x1b]0;pwned\x07file.txt";
|
||||
let clean = sanitize_for_terminal(dirty);
|
||||
assert_eq!(sanitize_for_terminal(&clean), clean);
|
||||
assert!(!clean.contains('\x1b'));
|
||||
assert!(!clean.contains('\x07'));
|
||||
}
|
||||
|
||||
+8
-4
@@ -29,7 +29,9 @@ use crate::error::*;
|
||||
use crate::input::OpenedInput;
|
||||
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
|
||||
use crate::output::OutputHandle;
|
||||
use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike};
|
||||
use crate::preprocessor::{
|
||||
expand_tabs, replace_nonprintable, sanitize_for_terminal, strip_ansi, strip_overstrike,
|
||||
};
|
||||
use crate::style::StyleComponent;
|
||||
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
||||
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
|
||||
@@ -489,7 +491,7 @@ impl Printer for InteractivePrinter<'_> {
|
||||
(but will be present if the output of 'bat' is piped). You can use 'bat -A' \
|
||||
to show the binary file contents.",
|
||||
Yellow.paint("[bat warning]"),
|
||||
input.description.summary(),
|
||||
sanitize_for_terminal(&input.description.summary()),
|
||||
)?;
|
||||
} else if self.config.style_components.grid() {
|
||||
self.print_horizontal_line(handle, '┬')?;
|
||||
@@ -543,9 +545,11 @@ impl Printer for InteractivePrinter<'_> {
|
||||
"{}{}{mode}",
|
||||
description
|
||||
.kind()
|
||||
.map(|kind| format!("{kind}: "))
|
||||
.map(|kind| format!("{}: ", sanitize_for_terminal(kind)))
|
||||
.unwrap_or_else(|| "".into()),
|
||||
self.colors.header_value.paint(description.title()),
|
||||
self.colors
|
||||
.header_value
|
||||
.paint(sanitize_for_terminal(description.title())),
|
||||
);
|
||||
self.print_header_multiline_component(handle, &header_filename)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user