fix(telegram): contain voice file extensions (#10456)

This commit is contained in:
Jasper
2026-08-02 07:48:38 -05:00
committed by GitHub
parent 20bb609c68
commit f1b811ae40
+88 -13
View File
@@ -256,19 +256,7 @@ impl TelegramGateway {
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700))?;
}
let ext = mime_type
.and_then(|m| m.rsplit('/').next())
.map(|sub| {
// Normalise common MIME sub-types to file extensions.
match sub {
"mpeg" => "mp3",
"mp4" | "x-m4a" => "m4a",
"ogg" => "ogg",
"wav" | "x-wav" => "wav",
other => other,
}
})
.unwrap_or("ogg");
let ext = Self::voice_file_extension(mime_type);
let filename = format!("voice_{}.{ext}", uuid::Uuid::new_v4());
let path = dir.join(filename);
@@ -284,6 +272,36 @@ impl TelegramGateway {
Ok(path)
}
fn voice_file_extension(mime_type: Option<&str>) -> String {
let media_type = mime_type
.and_then(|mime| mime.split(';').next())
.map(str::trim)
.map(str::to_ascii_lowercase);
let subtype = media_type
.as_deref()
.and_then(|mime| mime.strip_prefix("audio/"));
let Some(subtype) = subtype else {
return "ogg".to_string();
};
match subtype {
"mpeg" => "mp3".to_string(),
"mp4" | "x-m4a" => "m4a".to_string(),
"ogg" => "ogg".to_string(),
"wav" | "x-wav" | "vnd.wave" => "wav".to_string(),
other
if other.len() <= 16
&& other.bytes().enumerate().all(|(index, byte)| {
byte.is_ascii_alphanumeric() || (index > 0 && matches!(byte, b'-' | b'_'))
}) =>
{
other.to_string()
}
_ => "ogg".to_string(),
}
}
/// Build the text prompt that tells Goose about a voice message file.
fn voice_prompt(
path: &std::path::Path,
@@ -791,6 +809,63 @@ mod tests {
let _ = std::fs::remove_file(&path);
}
#[test]
fn voice_file_extension_preserves_safe_audio_formats() {
let cases = [
(Some("audio/mpeg"), "mp3"),
(Some("audio/mp4"), "m4a"),
(Some("audio/x-m4a"), "m4a"),
(Some("audio/ogg; codecs=opus"), "ogg"),
(Some("audio/x-wav"), "wav"),
(Some("audio/vnd.wave"), "wav"),
(Some("audio/flac"), "flac"),
(Some("audio/WEBM"), "webm"),
(Some("Audio/MPEG"), "mp3"),
(Some("AUDIO/WEBM"), "webm"),
];
for (mime_type, expected) in cases {
assert_eq!(TelegramGateway::voice_file_extension(mime_type), expected);
}
}
#[test]
fn voice_file_extension_rejects_filename_syntax() {
let invalid = [
None,
Some("application/ogg"),
Some("audio/..\\..\\outside"),
Some("audio/../../outside"),
Some("audio/ogg:stream"),
Some("audio/ogg.stream"),
Some("audio/ogg\nnext"),
Some("audio/ogg; touch=outside"),
Some("audio/ogg$HOME"),
Some("audio/åudio"),
Some("audio/this-subtype-is-too-long"),
];
for mime_type in invalid {
assert_eq!(TelegramGateway::voice_file_extension(mime_type), "ogg");
}
}
#[test]
fn save_voice_file_contains_untrusted_mime_before_pairing() {
let bytes = b"unpaired voice data";
let path = TelegramGateway::save_voice_file(bytes, Some("audio/..\\..\\outside")).unwrap();
let expected_dir = std::env::temp_dir().join("goose_voice");
assert_eq!(path.parent(), Some(expected_dir.as_path()));
let filename = path.file_name().unwrap().to_str().unwrap();
assert!(filename.starts_with("voice_"));
assert!(filename.ends_with(".ogg"));
assert!(!filename.chars().any(|c| matches!(c, '/' | '\\' | ':')));
assert_eq!(std::fs::read(&path).unwrap(), bytes);
let _ = std::fs::remove_file(&path);
}
#[test]
fn cleanup_preserves_recent_files() {
let dir = std::env::temp_dir().join("goose_voice");