Skip to content

Commit b50a811

Browse files
committed
fix(chat): wire MIME allowlist into config reload and reject malformed MIME types
- Apply chat.attachment_allowed_mime_types changes during hot reload. - Validate MIME strings have exactly two non-empty parts before allowlist matching. - Bump version to 0.1.107. Signed-off-by: Eli Ma <eli@patch.sh>
1 parent 92e214f commit b50a811

5 files changed

Lines changed: 89 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["bin"]
33

44
[package]
55
name = "monoengine-core"
6-
version = "0.1.106"
6+
version = "0.1.107"
77
edition = "2024"
88

99
[lib]

bin/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "monoengine"
3-
version = "0.1.106"
3+
version = "0.1.107"
44
edition = "2024"
55

66
# The thin composition-root binary. It depends on `monoengine-core` (which only

src/api/router/chat_router.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,12 @@ fn validate_chat_attachment_metadata(
7070
}
7171

7272
let file_type = file_type.trim();
73+
let mime_parts: Vec<&str> = file_type.split('/').collect();
7374
if file_type.is_empty()
7475
|| file_type.len() > CHAT_ATTACHMENT_MAX_FILE_TYPE_LEN
75-
|| !file_type.contains('/')
76+
|| mime_parts.len() != 2
77+
|| mime_parts[0].is_empty()
78+
|| mime_parts[1].is_empty()
7679
|| file_type.chars().any(char::is_control)
7780
{
7881
return Err(ApiError::bad_request(anyhow::anyhow!(
@@ -881,6 +884,20 @@ mod tests {
881884
assert!(validate_chat_attachment_metadata("x.txt", "text/plain", 0, &[]).is_err());
882885
assert!(validate_chat_attachment_metadata("x.txt", "not-a-mime", 1, &[]).is_err());
883886
assert!(validate_chat_attachment_metadata("x.txt", "text/plain", 1, &[]).is_ok());
887+
// Malformed MIME strings must be rejected even when an allowlist is present.
888+
assert!(
889+
validate_chat_attachment_metadata("x.png", "image/", 1, &["image/*".to_string()])
890+
.is_err()
891+
);
892+
assert!(
893+
validate_chat_attachment_metadata(
894+
"x.png",
895+
"image/png/extra",
896+
1,
897+
&["image/*".to_string()]
898+
)
899+
.is_err()
900+
);
884901
}
885902

886903
#[test]

src/config/reload.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use tokio::{
1515
use crate::{
1616
common::errors::MegaError,
1717
config::{
18-
ArtifactGcConfig, BuckConfig, Config, DEFAULT_MAIL_TEMPLATE_LOCALE,
18+
ArtifactGcConfig, BuckConfig, ChatConfig, Config, DEFAULT_MAIL_TEMPLATE_LOCALE,
1919
DEFAULT_NOTIFICATION_DELIVERY_MODE, LogConfig, MailConfig, NotificationConfig,
2020
},
2121
};
@@ -146,6 +146,7 @@ impl ConfigHandle {
146146
&mut next.notification,
147147
&mut report,
148148
);
149+
apply_chat_changes(&current.chat, &candidate.chat, &mut next.chat, &mut report);
149150
collect_database_restart_fields(&current, &candidate, &mut report);
150151
collect_redis_restart_fields(&current, &candidate, &mut report);
151152
collect_static_restart_fields(&current, &candidate, &mut report);
@@ -679,6 +680,36 @@ fn notification_default_locale(config: &Option<NotificationConfig>) -> String {
679680
.unwrap_or_else(|| DEFAULT_MAIL_TEMPLATE_LOCALE.to_string())
680681
}
681682

683+
/// `chat.attachment_allowed_mime_types` is read live by the attachment handlers,
684+
/// so changes can be hot-applied without a restart.
685+
fn apply_chat_changes(
686+
current: &Option<ChatConfig>,
687+
candidate: &Option<ChatConfig>,
688+
next: &mut Option<ChatConfig>,
689+
report: &mut ConfigReloadReport,
690+
) {
691+
if current == candidate {
692+
return;
693+
}
694+
695+
let current_list = chat_mime_allowlist(current);
696+
let candidate_list = chat_mime_allowlist(candidate);
697+
*next = candidate.clone();
698+
699+
if current_list != candidate_list {
700+
report
701+
.applied_fields
702+
.push("chat.attachment_allowed_mime_types");
703+
}
704+
}
705+
706+
fn chat_mime_allowlist(config: &Option<ChatConfig>) -> Vec<String> {
707+
config
708+
.as_ref()
709+
.map(|c| c.attachment_allowed_mime_types.clone())
710+
.unwrap_or_default()
711+
}
712+
682713
fn collect_artifact_gc_restart_fields(
683714
current: &ArtifactGcConfig,
684715
candidate: &ArtifactGcConfig,
@@ -1422,6 +1453,41 @@ mod tests {
14221453
assert!(!snapshot.mail.as_ref().expect("mail config").enabled);
14231454
}
14241455

1456+
#[test]
1457+
fn reload_applies_chat_mime_allowlist_without_restart() {
1458+
let temp_dir = tempfile::tempdir().expect("temp dir");
1459+
let mut current = isolated_config(temp_dir.path().join("current"));
1460+
current.chat = Some(ChatConfig::default());
1461+
let handle = ConfigHandle::new(current);
1462+
1463+
let mut candidate = handle.snapshot().expect("snapshot").as_ref().clone();
1464+
candidate
1465+
.chat
1466+
.as_mut()
1467+
.expect("chat config")
1468+
.attachment_allowed_mime_types =
1469+
vec!["image/*".to_string(), "application/pdf".to_string()];
1470+
1471+
let report = handle.reload(candidate).expect("reload should succeed");
1472+
let snapshot = handle.snapshot().expect("snapshot after reload");
1473+
1474+
assert_eq!(
1475+
report.applied_fields,
1476+
vec!["chat.attachment_allowed_mime_types"]
1477+
);
1478+
assert!(report.restart_required_fields.is_empty());
1479+
assert!(report.applied());
1480+
assert!(!report.requires_restart());
1481+
assert_eq!(
1482+
snapshot
1483+
.chat
1484+
.as_ref()
1485+
.expect("chat config")
1486+
.attachment_allowed_mime_types,
1487+
vec!["image/*".to_string(), "application/pdf".to_string()]
1488+
);
1489+
}
1490+
14251491
#[test]
14261492
fn reload_applies_mail_dispatcher_limits_without_restart() {
14271493
let temp_dir = tempfile::tempdir().expect("temp dir");

0 commit comments

Comments
 (0)