Skip to content

Commit 31d2e1a

Browse files
committed
Fix #2033: Web Chat id Path Traversal
1 parent 604368e commit 31d2e1a

4 files changed

Lines changed: 78 additions & 2 deletions

File tree

mistralrs-cli/src/ui/handlers/api.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::ui::types::{
2121
AppState, ChatFile, DeleteChatRequest, LoadChatRequest, NewChatRequest, RenameChatRequest,
2222
SelectRequest,
2323
};
24-
use crate::ui::utils::get_cache_dir;
24+
use crate::ui::utils::{get_cache_dir, is_chat_id_safe};
2525

2626
fn validate_image_upload(
2727
filename: Option<&str>,
@@ -373,6 +373,9 @@ pub async fn delete_chat(
373373
Extension(app): Extension<Arc<AppState>>,
374374
Json(req): Json<DeleteChatRequest>,
375375
) -> impl IntoResponse {
376+
if !is_chat_id_safe(&req.id) {
377+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
378+
}
376379
let path = format!("{}/{}.json", app.chats_dir, req.id);
377380
match fs::remove_file(&path).await {
378381
Ok(_) => (StatusCode::OK, "Deleted").into_response(),
@@ -384,6 +387,9 @@ pub async fn load_chat(
384387
Extension(app): Extension<Arc<AppState>>,
385388
Json(req): Json<LoadChatRequest>,
386389
) -> impl IntoResponse {
390+
if !is_chat_id_safe(&req.id) {
391+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
392+
}
387393
let path = format!("{}/{}.json", app.chats_dir, req.id);
388394
if let Ok(bytes) = fs::read(&path).await {
389395
if let Ok(chat) = serde_json::from_slice::<ChatFile>(&bytes) {
@@ -399,6 +405,9 @@ pub async fn rename_chat(
399405
Extension(app): Extension<Arc<AppState>>,
400406
Json(req): Json<RenameChatRequest>,
401407
) -> impl IntoResponse {
408+
if !is_chat_id_safe(&req.id) {
409+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
410+
}
402411
let path = format!("{}/{}.json", app.chats_dir, req.id);
403412
if let Ok(bytes) = fs::read(&path).await {
404413
if let Ok(mut chat) = serde_json::from_slice::<ChatFile>(&bytes) {
@@ -427,6 +436,9 @@ pub async fn append_message(
427436
Extension(app): Extension<Arc<AppState>>,
428437
Json(req): Json<AppendMessageRequest>,
429438
) -> impl IntoResponse {
439+
if !is_chat_id_safe(&req.id) {
440+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
441+
}
430442
if let Err(e) = append_chat_message(&app, &req.id, &req.role, &req.content, req.images).await {
431443
error!("append message error: {}", e);
432444
return (StatusCode::INTERNAL_SERVER_ERROR, "append failed").into_response();

mistralrs-cli/src/ui/utils.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,29 @@ pub fn get_cache_dir() -> PathBuf {
1212
});
1313
cache_home.join("mistralrs")
1414
}
15+
16+
/// Check if a chat ID is safe to use in a filename.
17+
pub fn is_chat_id_safe(id: &str) -> bool {
18+
if id.is_empty() {
19+
return false;
20+
}
21+
id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
22+
}
23+
24+
#[cfg(test)]
25+
mod tests {
26+
use super::*;
27+
28+
#[test]
29+
fn test_is_chat_id_safe() {
30+
assert!(is_chat_id_safe("chat_1"));
31+
assert!(is_chat_id_safe("chat-2"));
32+
assert!(is_chat_id_safe("chat_3-4"));
33+
assert!(!is_chat_id_safe("../etc/passwd"));
34+
assert!(!is_chat_id_safe("..\\windows\\system32"));
35+
assert!(!is_chat_id_safe("chat.json"));
36+
assert!(!is_chat_id_safe("chat 1"));
37+
assert!(!is_chat_id_safe("chat/1"));
38+
assert!(!is_chat_id_safe(""));
39+
}
40+
}

mistralrs-web-chat/src/handlers/api.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::types::{
2727
// Append partial assistant messages
2828
// (defined below)
2929
};
30-
use crate::utils::get_cache_dir;
30+
use crate::utils::{get_cache_dir, is_chat_id_safe};
3131
use serde::Deserialize;
3232

3333
fn validate_image_upload(
@@ -458,6 +458,9 @@ pub async fn delete_chat(
458458
State(app): State<Arc<AppState>>,
459459
Json(req): Json<DeleteChatRequest>,
460460
) -> impl IntoResponse {
461+
if !is_chat_id_safe(&req.id) {
462+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
463+
}
461464
let path = format!("{}/{}.json", app.chats_dir, req.id);
462465
if let Err(e) = tokio::fs::remove_file(&path).await {
463466
error!("delete chat error: {}", e);
@@ -478,6 +481,9 @@ pub async fn load_chat(
478481
State(app): State<Arc<AppState>>,
479482
Json(req): Json<LoadChatRequest>,
480483
) -> impl IntoResponse {
484+
if !is_chat_id_safe(&req.id) {
485+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
486+
}
481487
let path = format!("{}/{}.json", app.chats_dir, req.id);
482488
match fs::read(&path).await {
483489
Ok(data) => match serde_json::from_slice::<ChatFile>(&data) {
@@ -510,6 +516,9 @@ pub async fn rename_chat(
510516
State(app): State<Arc<AppState>>,
511517
Json(req): Json<RenameChatRequest>,
512518
) -> impl IntoResponse {
519+
if !is_chat_id_safe(&req.id) {
520+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
521+
}
513522
let path = format!("{}/{}.json", app.chats_dir, req.id);
514523
if let Ok(data) = fs::read(&path).await {
515524
if let Ok(mut chat) = serde_json::from_slice::<ChatFile>(&data) {
@@ -538,6 +547,9 @@ pub async fn append_message(
538547
State(app): State<Arc<AppState>>,
539548
Json(req): Json<AppendMessageRequest>,
540549
) -> impl IntoResponse {
550+
if !is_chat_id_safe(&req.id) {
551+
return (StatusCode::BAD_REQUEST, "invalid chat id").into_response();
552+
}
541553
if let Err(e) =
542554
crate::chat::append_chat_message(&app, &req.id, &req.role, &req.content, req.images).await
543555
{

mistralrs-web-chat/src/utils.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,29 @@ pub fn get_cache_dir() -> PathBuf {
1313
});
1414
cache_home.join("mistralrs-web-chat")
1515
}
16+
17+
/// Check if a chat ID is safe to use in a filename.
18+
pub fn is_chat_id_safe(id: &str) -> bool {
19+
if id.is_empty() {
20+
return false;
21+
}
22+
id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
23+
}
24+
25+
#[cfg(test)]
26+
mod tests {
27+
use super::*;
28+
29+
#[test]
30+
fn test_is_chat_id_safe() {
31+
assert!(is_chat_id_safe("chat_1"));
32+
assert!(is_chat_id_safe("chat-2"));
33+
assert!(is_chat_id_safe("chat_3-4"));
34+
assert!(!is_chat_id_safe("../etc/passwd"));
35+
assert!(!is_chat_id_safe("..\\windows\\system32"));
36+
assert!(!is_chat_id_safe("chat.json"));
37+
assert!(!is_chat_id_safe("chat 1"));
38+
assert!(!is_chat_id_safe("chat/1"));
39+
assert!(!is_chat_id_safe(""));
40+
}
41+
}

0 commit comments

Comments
 (0)