hardening: fix 7 production weaknesses found in codebase scan
Scan findings and fixes:
1. Gateway buffer overflow (8KB → 64KB)
- Fixed: Increased request buffer from 8,192 to 65,536 bytes
- Large POST bodies (long prompts) were silently truncated
2. Gateway slow-loris attack (no read timeout → 30s)
- Fixed: tokio::time::timeout(30s) on stream.read()
- Malicious clients could hold connections indefinitely
3. Webhook secret timing attack (== → constant_time_eq)
- Fixed: Now uses constant_time_eq() for secret comparison
- Prevents timing side-channel on webhook authentication
4. Pairing brute force (no limit → 5 attempts + 5min lockout)
- Fixed: PairingGuard tracks failed attempts with lockout
- Returns 429 Too Many Requests with retry_after seconds
5. Shell tool hang (no timeout → 60s kill)
- Fixed: tokio::time::timeout(60s) on Command::output()
- Commands that hang are killed and return error
6. Shell tool OOM (unbounded output → 1MB cap)
- Fixed: stdout/stderr truncated at 1MB with warning
- Prevents memory exhaustion from verbose commands
7. Provider HTTP timeout (none → 120s request + 10s connect)
- Fixed: All 5 providers (OpenRouter, Anthropic, OpenAI,
Ollama, Compatible) now have reqwest timeouts
- Ollama gets 300s (local models are slower)
949 tests passing, 0 clippy warnings, cargo fmt clean
This commit is contained in:
parent
0b5b49537a
commit
976c5bbf3c
8 changed files with 219 additions and 49 deletions
|
|
@ -3,6 +3,12 @@ use crate::security::SecurityPolicy;
|
|||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Maximum shell command execution time before kill.
|
||||
const SHELL_TIMEOUT_SECS: u64 = 60;
|
||||
/// Maximum output size in bytes (1MB).
|
||||
const MAX_OUTPUT_BYTES: usize = 1_048_576;
|
||||
|
||||
/// Shell command execution tool with sandboxing
|
||||
pub struct ShellTool {
|
||||
|
|
@ -53,25 +59,55 @@ impl Tool for ShellTool {
|
|||
});
|
||||
}
|
||||
|
||||
let output = tokio::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(command)
|
||||
.current_dir(&self.security.workspace_dir)
|
||||
.output()
|
||||
.await?;
|
||||
// Execute with timeout to prevent hanging commands
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(SHELL_TIMEOUT_SECS),
|
||||
tokio::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(command)
|
||||
.current_dir(&self.security.workspace_dir)
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let mut stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
Ok(ToolResult {
|
||||
success: output.status.success(),
|
||||
output: stdout,
|
||||
error: if stderr.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(stderr)
|
||||
},
|
||||
})
|
||||
// Truncate output to prevent OOM
|
||||
if stdout.len() > MAX_OUTPUT_BYTES {
|
||||
stdout.truncate(MAX_OUTPUT_BYTES);
|
||||
stdout.push_str("\n... [output truncated at 1MB]");
|
||||
}
|
||||
if stderr.len() > MAX_OUTPUT_BYTES {
|
||||
stderr.truncate(MAX_OUTPUT_BYTES);
|
||||
stderr.push_str("\n... [stderr truncated at 1MB]");
|
||||
}
|
||||
|
||||
Ok(ToolResult {
|
||||
success: output.status.success(),
|
||||
output: stdout,
|
||||
error: if stderr.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(stderr)
|
||||
},
|
||||
})
|
||||
}
|
||||
Ok(Err(e)) => Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Failed to execute command: {e}")),
|
||||
}),
|
||||
Err(_) => Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!(
|
||||
"Command timed out after {SHELL_TIMEOUT_SECS}s and was killed"
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue