mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 09:09:19 +00:00
fix: 5 critical/high-priority bugs (auth bypass, relay failures, unbounded recursion, context growth) (#1083)
* fix: address 5 critical and high-priority bugs from issue tracker - #1033: reject webhook requests when secret is cleared at runtime via update_secret(None), preventing auth bypass through SIGHUP hot-swap - #908: reset consecutive_failures counter on successful SSE stream reconnection in relay channel, so circuit breaker counts truly consecutive failures - #975: add depth limit (16) to validate_tool_schema() to prevent stack overflow on deeply nested schemas - #974: add depth limit (8) to resolve_nested() to prevent stack overflow on deeply nested capabilities wrappers - #826: truncate oversized tool outputs (>8KB) in routine lightweight loop to prevent unbounded context growth across iterations Each fix includes a regression test. Closes #1033, #908, #975, #974, #826 Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: 5 more high-priority bugs (routine cache, job signals, input limits) - #1077: recompute next_fire_at when re-enabling cron routines via web toggle, mirroring CLI behavior so cron ticker picks them up - #1076: refresh event trigger cache after web toggle/delete operations so event/system_event routines reflect changes immediately - #892: remove Stuck from check_signals() stop-states in JobDelegate since Stuck is recoverable (Stuck -> InProgress via self-repair) - #976: truncate oversized description strings in CapabilitiesFile to 4KB to prevent memory abuse from malicious capabilities files - #977: drop oversized parameters schema JSON (>64KB) in CapabilitiesFile to prevent unbounded memory growth Each fix includes regression tests where applicable. Closes #1077, #1076, #892, #976, #977 Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: prevent ReDoS in event trigger regex patterns - #825: use RegexBuilder with 64KB size limit when compiling user-supplied event trigger patterns, both at creation time (routine tool) and at cache refresh (routine engine) Note: Rust's regex crate already guarantees O(n) matching, so the size limit prevents excessive memory use during compilation rather than catastrophic backtracking at match time. Closes #825 Co-Authored-By: Claude Opus 4.6 <[email protected]> * Harden HTTP SSRF IP filtering * Apply rustfmt after staging merge --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1e00b1fed5
commit
e805ec61aa
+48
-3
@@ -430,9 +430,24 @@ pub fn redact_params(params: &serde_json::Value, sensitive: &[&str]) -> serde_js
|
||||
/// Properties without a `"type"` field are allowed (freeform/any-type).
|
||||
/// This is an intentional pattern used by tools like `json` and `http` for
|
||||
/// OpenAI compatibility, since union types with arrays require `items`.
|
||||
/// Maximum nesting depth for tool schema validation to prevent stack overflow
|
||||
/// on maliciously crafted schemas.
|
||||
const MAX_SCHEMA_DEPTH: usize = 16;
|
||||
|
||||
pub fn validate_tool_schema(schema: &serde_json::Value, path: &str) -> Vec<String> {
|
||||
validate_tool_schema_inner(schema, path, 0)
|
||||
}
|
||||
|
||||
fn validate_tool_schema_inner(schema: &serde_json::Value, path: &str, depth: usize) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if depth > MAX_SCHEMA_DEPTH {
|
||||
errors.push(format!(
|
||||
"{path}: schema nesting exceeds maximum depth of {MAX_SCHEMA_DEPTH}"
|
||||
));
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Rule 1: must have "type": "object" at this level
|
||||
match schema.get("type").and_then(|t| t.as_str()) {
|
||||
Some("object") => {}
|
||||
@@ -474,14 +489,17 @@ pub fn validate_tool_schema(schema: &serde_json::Value, path: &str) -> Vec<Strin
|
||||
if let Some(prop_type) = prop.get("type").and_then(|t| t.as_str()) {
|
||||
match prop_type {
|
||||
"object" => {
|
||||
errors.extend(validate_tool_schema(prop, &prop_path));
|
||||
errors.extend(validate_tool_schema_inner(prop, &prop_path, depth + 1));
|
||||
}
|
||||
"array" => {
|
||||
if let Some(items) = prop.get("items") {
|
||||
// If items is an object type, recurse
|
||||
if items.get("type").and_then(|t| t.as_str()) == Some("object") {
|
||||
errors
|
||||
.extend(validate_tool_schema(items, &format!("{prop_path}.items")));
|
||||
errors.extend(validate_tool_schema_inner(
|
||||
items,
|
||||
&format!("{prop_path}.items"),
|
||||
depth + 1,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
errors.push(format!("{prop_path}: array property missing \"items\""));
|
||||
@@ -810,6 +828,33 @@ mod tests {
|
||||
assert!(errors[0].contains("\"missing_field\""));
|
||||
}
|
||||
|
||||
/// Regression test for issue #975: deeply nested schemas must not cause
|
||||
/// stack overflow. The validator should stop at MAX_SCHEMA_DEPTH and
|
||||
/// report an error instead of recursing infinitely.
|
||||
#[test]
|
||||
fn test_validate_schema_depth_limit() {
|
||||
// Build a schema nested 20 levels deep (exceeds MAX_SCHEMA_DEPTH=16)
|
||||
let mut schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leaf": { "type": "string" }
|
||||
}
|
||||
});
|
||||
for _ in 0..20 {
|
||||
schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nested": schema
|
||||
}
|
||||
});
|
||||
}
|
||||
let errors = validate_tool_schema(&schema, "test");
|
||||
assert!(
|
||||
errors.iter().any(|e| e.contains("maximum depth")),
|
||||
"expected depth limit error, got: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approval_context_autonomous_allows_unless_auto_approved() {
|
||||
let ctx = ApprovalContext::autonomous();
|
||||
|
||||
Reference in New Issue
Block a user