Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 70 additions & 12 deletions crates/rust-mcp-sdk/src/mcp_http/mcp_http_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,13 @@ impl McpHttpHandler {
return error_response(StatusCode::BAD_REQUEST, error);
}

let session_id: Option<SessionId> = headers
.get(MCP_SESSION_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
let session_id = match parse_session_id_header(headers) {
Ok(id) => id,
Err(()) => {
let error = SdkError::bad_request().with_message("Invalid Mcp-Session-Id header");
return error_response(StatusCode::BAD_REQUEST, error);
}
};

let payload = request.body();

Expand Down Expand Up @@ -409,10 +412,13 @@ impl McpHttpHandler {
return error_response(StatusCode::BAD_REQUEST, error);
}

let session_id: Option<SessionId> = headers
.get(MCP_SESSION_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
let session_id = match parse_session_id_header(headers) {
Ok(id) => id,
Err(()) => {
let error = SdkError::bad_request().with_message("Invalid Mcp-Session-Id header");
return error_response(StatusCode::BAD_REQUEST, error);
}
};

let last_event_id: Option<SessionId> = headers
.get(MCP_LAST_EVENT_ID_HEADER)
Expand Down Expand Up @@ -447,10 +453,13 @@ impl McpHttpHandler {
return error_response(StatusCode::BAD_REQUEST, error);
}

let session_id: Option<SessionId> = headers
.get(MCP_SESSION_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
let session_id = match parse_session_id_header(headers) {
Ok(id) => id,
Err(()) => {
let error = SdkError::bad_request().with_message("Invalid Mcp-Session-Id header");
return error_response(StatusCode::BAD_REQUEST, error);
}
};

let response = match session_id {
Some(id) => delete_session(id, state).await,
Expand All @@ -463,3 +472,52 @@ impl McpHttpHandler {
response
}
}

/// Maximum accepted length (in bytes) of the `Mcp-Session-Id` header.
const MAX_SESSION_ID_LEN: usize = 128;

/// Returns true if the session id is non-empty, within the length cap, and uses
/// only URL-safe characters (covers UUIDs, base64url, and prefixed ids).
fn is_valid_session_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_SESSION_ID_LEN
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~'))
}

/// Extracts and validates the `Mcp-Session-Id` header.
///
/// Returns `Ok(None)` when the header is absent, `Ok(Some(id))` when present and
/// valid, and `Err(())` when present but oversized or malformed. Validating up
/// front avoids unbounded allocations and map lookups from a hostile value.
fn parse_session_id_header(headers: &HeaderMap) -> Result<Option<SessionId>, ()> {
match headers.get(MCP_SESSION_ID_HEADER) {
None => Ok(None),
Some(value) => match value.to_str() {
Ok(s) if is_valid_session_id(s) => Ok(Some(s.to_string())),
_ => Err(()),
},
}
}

#[cfg(test)]
mod session_id_tests {
use super::*;

#[test]
fn accepts_uuid_and_prefixed_ids() {
assert!(is_valid_session_id("550e8400-e29b-41d4-a716-446655440000"));
assert!(is_valid_session_id("tsk_abcDEF123"));
assert!(is_valid_session_id("s_0001"));
}

#[test]
fn rejects_empty_oversized_and_bad_charset() {
assert!(!is_valid_session_id(""));
assert!(!is_valid_session_id(&"a".repeat(MAX_SESSION_ID_LEN + 1)));
assert!(!is_valid_session_id("has space"));
assert!(!is_valid_session_id("../etc/passwd"));
assert!(!is_valid_session_id("naïve"));
}
}
23 changes: 23 additions & 0 deletions crates/rust-mcp-sdk/tests/test_streamable_http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,29 @@ async fn should_handle_elicitation() {
server.hyper_runtime.await_server().await.unwrap();
}

// should reject an oversized / malformed Mcp-Session-Id header
#[tokio::test]
async fn should_reject_malformed_session_id_header() {
let (server, _session_id) = initialize_server(None, None).await.unwrap();

let oversized = "a".repeat(200);
let mut headers = HashMap::new();
headers.insert("Accept", "text/event-stream");
headers.insert("mcp-protocol-version", "2025-03-26");
headers.insert("mcp-session-id", oversized.as_str());

let response = send_get_request(&server.streamable_url, Some(headers))
.await
.unwrap();

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let error_data: SdkError = response.json().await.unwrap();
assert!(error_data.message.contains("Invalid Mcp-Session-Id header"));

server.hyper_runtime.graceful_shutdown(ONE_MILLISECOND);
server.hyper_runtime.await_server().await.unwrap()
}

// should return 400 error for invalid JSON-RPC messages
// should keep stream open after sending server notifications
// NA: should reject second initialization request
Expand Down
Loading