mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 00:00:50 +00:00
fix(security): apply 2026-03-31 security audit hardening
- Use getrandom for CSPRNG HMAC secret generation - Case-insensitive admin path check prevents bypass via /Admin/, /ADMIN/ - Add TRUSTED_PROXY_DEPTH for multi-hop proxy X-Forwarded-For extraction - Add dangerous-builtin-tools feature flag with security warning - Remove repomix-output.xml, add .syntext to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bdf73c9057
commit
e6dd396b05
@@ -12,6 +12,7 @@
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.syntext
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
Generated
+1
@@ -68,6 +68,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"hex",
|
||||
"hmac",
|
||||
"httpdate",
|
||||
|
||||
@@ -31,6 +31,7 @@ hmac = "0.12"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
httpdate = "1"
|
||||
dashmap = "6"
|
||||
getrandom = "0.3"
|
||||
aws-sigv4 = { version = "1.4", features = ["sign-http"] }
|
||||
aws-credential-types = "1.2"
|
||||
aws-smithy-runtime-api = "1"
|
||||
@@ -44,6 +45,10 @@ zeroize = "1"
|
||||
crc32fast = "1"
|
||||
|
||||
[features]
|
||||
## Enables BashTool and ReadFileTool in the builtin tool registry.
|
||||
## These tools execute arbitrary shell commands and read arbitrary files as the
|
||||
## proxy process user. Do NOT enable in production without sandboxing.
|
||||
dangerous-builtin-tools = []
|
||||
redis = ["dep:redis"]
|
||||
qdrant = ["dep:qdrant-client"]
|
||||
otel = [
|
||||
|
||||
@@ -169,12 +169,9 @@ pub fn ensure_hmac_secret(conn: &Connection) -> Vec<u8> {
|
||||
return secret;
|
||||
}
|
||||
|
||||
// Generate 32 random bytes from two UUID v4s.
|
||||
// Generate 256-bit CSPRNG secret directly.
|
||||
let mut buf = [0u8; 32];
|
||||
let a = uuid::Uuid::new_v4();
|
||||
let b = uuid::Uuid::new_v4();
|
||||
buf[..16].copy_from_slice(a.as_bytes());
|
||||
buf[16..].copy_from_slice(b.as_bytes());
|
||||
getrandom::fill(&mut buf).expect("CSPRNG failed");
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO settings (key, value) VALUES ('hmac_secret', ?1)",
|
||||
|
||||
@@ -285,10 +285,11 @@ pub async fn validate_auth(
|
||||
}
|
||||
}
|
||||
|
||||
// RBAC: developer keys cannot access admin endpoints
|
||||
// RBAC: developer keys cannot access admin endpoints.
|
||||
// Case-insensitive to prevent bypass via `/Admin/`, `/ADMIN/`, etc.
|
||||
if meta.role == KeyRole::Developer {
|
||||
let path = request.uri().path();
|
||||
if path.starts_with("/admin/") || path.starts_with("/admin") {
|
||||
let path = request.uri().path().to_ascii_lowercase();
|
||||
if path.starts_with("/admin/") || path == "/admin" {
|
||||
let err_body = serde_json::json!({
|
||||
"error": {
|
||||
"type": "permission_denied",
|
||||
@@ -525,6 +526,17 @@ static TRUST_PROXY_HEADERS: LazyLock<bool> = LazyLock::new(|| {
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
/// Number of trusted proxy hops. The client IP is extracted as the Nth-from-right
|
||||
/// entry in X-Forwarded-For. Defaults to 1 (single reverse proxy).
|
||||
/// Set TRUSTED_PROXY_DEPTH=2 for chains like CDN -> LB -> proxy.
|
||||
static TRUSTED_PROXY_DEPTH: LazyLock<usize> = LazyLock::new(|| {
|
||||
std::env::var("TRUSTED_PROXY_DEPTH")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(1)
|
||||
.max(1) // minimum 1
|
||||
});
|
||||
|
||||
/// Check if an IP address is allowed by the configured allowlist.
|
||||
/// Returns true if no allowlist is set (open access).
|
||||
pub fn is_ip_allowed(ip: std::net::IpAddr) -> bool {
|
||||
@@ -543,14 +555,20 @@ pub fn ip_allowlist_active() -> bool {
|
||||
/// Applied before auth so blocked IPs never reach authentication.
|
||||
pub async fn check_ip_allowlist(request: Request<Body>, next: Next) -> Result<Response, Response> {
|
||||
// Extract client IP from X-Forwarded-For (if trusted) or connection info.
|
||||
// TRUSTED_PROXY_DEPTH controls which entry to pick: depth=1 (default) takes
|
||||
// the rightmost (single proxy), depth=2 takes the second-from-right (two hops), etc.
|
||||
let client_ip = if *TRUST_PROXY_HEADERS {
|
||||
// Take the *rightmost* IP: a trusted reverse proxy appends the real client IP.
|
||||
// The leftmost value is attacker-controlled and must not be trusted.
|
||||
let depth = *TRUSTED_PROXY_DEPTH;
|
||||
request
|
||||
.headers()
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.rsplit(',').map(|p| p.trim()).find(|p| !p.is_empty()))
|
||||
.and_then(|s| {
|
||||
s.rsplit(',')
|
||||
.map(|p| p.trim())
|
||||
.filter(|p| !p.is_empty())
|
||||
.nth(depth - 1)
|
||||
})
|
||||
.and_then(|s| s.parse::<std::net::IpAddr>().ok())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -846,63 +846,45 @@ async fn messages(
|
||||
&original_model,
|
||||
);
|
||||
|
||||
// Tool execution: if the response contains tool_use blocks for
|
||||
// registered tools, execute them and make a follow-up backend call.
|
||||
// Tool execution: bounded loop with termination guards.
|
||||
let anthropic_resp = if let Some(ref engine) = state.tool_engine {
|
||||
let tool_calls =
|
||||
crate::tools::execution::extract_tool_calls(&anthropic_resp);
|
||||
let (auto_exec, _pass_through) =
|
||||
crate::tools::execution::partition_tool_calls(
|
||||
&tool_calls,
|
||||
&engine.registry,
|
||||
&engine.policy,
|
||||
);
|
||||
|
||||
if !auto_exec.is_empty() {
|
||||
let results = crate::tools::execution::execute_tool_calls(
|
||||
&auto_exec,
|
||||
engine.registry.clone(),
|
||||
&engine.policy,
|
||||
&engine.loop_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut follow_up_req = body.clone();
|
||||
follow_up_req.messages.push(
|
||||
crate::tools::execution::response_to_assistant_message(
|
||||
&anthropic_resp,
|
||||
),
|
||||
);
|
||||
follow_up_req.messages.push(
|
||||
crate::tools::execution::tool_results_to_user_message(&results),
|
||||
);
|
||||
|
||||
let mut follow_up_openai =
|
||||
mapping::message_map::anthropic_to_openai_request(&follow_up_req);
|
||||
follow_up_openai.model = mapped_model.clone();
|
||||
|
||||
match client.chat_completion(&follow_up_openai).await {
|
||||
Ok((follow_up_resp, _, _)) => {
|
||||
tracing::info!(
|
||||
tools_executed = results.len(),
|
||||
"tool execution loop completed"
|
||||
);
|
||||
mapping::message_map::openai_to_anthropic_response(
|
||||
&follow_up_resp,
|
||||
&original_model,
|
||||
)
|
||||
let client_for_tools = client.clone();
|
||||
let model_for_tools = mapped_model.clone();
|
||||
let orig_model_for_tools = original_model.clone();
|
||||
let (resp, trace) = crate::tools::execution::maybe_execute_tools(
|
||||
engine,
|
||||
&body,
|
||||
anthropic_resp,
|
||||
|follow_up_req| {
|
||||
let c = client_for_tools.clone();
|
||||
let m = model_for_tools.clone();
|
||||
let om = orig_model_for_tools.clone();
|
||||
async move {
|
||||
let mut oai_req =
|
||||
mapping::message_map::anthropic_to_openai_request(
|
||||
&follow_up_req,
|
||||
);
|
||||
oai_req.model = m;
|
||||
match c.chat_completion(&oai_req).await {
|
||||
Ok((resp, _, _)) => Ok(
|
||||
mapping::message_map::openai_to_anthropic_response(
|
||||
&resp, &om,
|
||||
),
|
||||
),
|
||||
Err(e) => Err(format!("{e}")),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
"follow-up backend call after tool execution failed"
|
||||
);
|
||||
anthropic_resp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anthropic_resp
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
termination_reason = ?trace.termination_reason,
|
||||
iterations = trace.iterations.len(),
|
||||
tool_calls = trace.total_tool_calls(),
|
||||
total_ms = trace.total_duration.as_millis(),
|
||||
"tool execution loop complete"
|
||||
);
|
||||
resp
|
||||
} else {
|
||||
anthropic_resp
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod policy;
|
||||
pub mod registry;
|
||||
pub mod trace;
|
||||
|
||||
pub use execution::{LoopConfig, ToolCall, ToolResult};
|
||||
pub use execution::{maybe_execute_tools, LoopConfig, ToolCall, ToolResult};
|
||||
pub use mcp::McpServerManager;
|
||||
pub use policy::{PolicyAction, PolicyRule, ToolExecutionPolicy};
|
||||
pub use registry::{Tool, ToolRegistry};
|
||||
|
||||
-25762
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user