From b1ed5d75b4d888014c95f85e2b2baa49057ade83 Mon Sep 17 00:00:00 2001 From: Olen Latham Date: Sat, 21 Feb 2026 21:41:11 -0600 Subject: [PATCH] fix: add API body size limits and memory content validation Add a 10MB DefaultBodyLimit to the API router to prevent unbounded request bodies (especially multipart file uploads) from exhausting server memory. Add input validation to the memory_save tool: - Content size capped at 50KB to prevent database/embedding bloat - Empty content rejected - Importance score validated to 0.0-1.0 range at runtime Files changed: - api/server.rs: add DefaultBodyLimit::max(10MB) layer - tools/memory_save.rs: add MAX_MEMORY_CONTENT_BYTES constant, content size check, empty check, importance range validation --- src/api/server.rs | 3 ++- src/tools/memory_save.rs | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/api/server.rs b/src/api/server.rs index 39dcc9d64..20d15ded9 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -7,7 +7,7 @@ use super::{ }; use axum::Json; -use axum::extract::Request; +use axum::extract::{DefaultBodyLimit, Request}; use axum::Router; use axum::http::{StatusCode, Uri, header}; use axum::middleware::{self, Next}; @@ -148,6 +148,7 @@ pub async fn start_http_server( .route("/update/apply", post(settings::update_apply)) .route("/webchat/send", post(webchat::webchat_send)) .route("/webchat/history", get(webchat::webchat_history)) + .layer(DefaultBodyLimit::max(10 * 1024 * 1024)) .layer(middleware::from_fn_with_state( state.clone(), api_auth_middleware, diff --git a/src/tools/memory_save.rs b/src/tools/memory_save.rs index 7563d8fc6..94e2077a6 100644 --- a/src/tools/memory_save.rs +++ b/src/tools/memory_save.rs @@ -9,6 +9,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; +/// Maximum allowed memory content length (bytes). Prevents oversized memories +/// from bloating the database and embedding index. +const MAX_MEMORY_CONTENT_BYTES: usize = 50_000; + /// Tool for saving memories to the store. #[derive(Debug, Clone)] pub struct MemorySaveTool { @@ -160,6 +164,25 @@ impl Tool for MemorySaveTool { } async fn call(&self, args: Self::Args) -> std::result::Result { + if args.content.is_empty() { + return Err(MemorySaveError("content must not be empty".into())); + } + + if args.content.len() > MAX_MEMORY_CONTENT_BYTES { + return Err(MemorySaveError(format!( + "content exceeds maximum length of {MAX_MEMORY_CONTENT_BYTES} bytes (got {})", + args.content.len() + ))); + } + + if let Some(importance) = args.importance { + if !(0.0..=1.0).contains(&importance) { + return Err(MemorySaveError(format!( + "importance must be between 0.0 and 1.0 (got {importance})" + ))); + } + } + // Parse memory type let memory_type = match args.memory_type.as_str() { "fact" => MemoryType::Fact,