Skip to content

Commit 8708b9b

Browse files
fix: address 18 MEDIUM/LOW security findings from deep audit
Identity (V04, V05, V07): - Block ':*' empty-prefix wildcard acting as hidden superuser - Catch specific crypto exceptions instead of bare except - Guard reactivation of security-suspended identities Handshake (V06, V10): - Use registry trust score over self-reported value - Cap pending challenges at 1000 with expired-challenge cleanup Transport (V19, V20, V21): - Default use_tls=True for all transports - Bound asyncio.Queue to 10k for gRPC and WebSocket MCP (V18, V22, V25): - SSRF guard: validate URL scheme, block loopback/internal hosts - Evict expired entries from _verified_clients cache (cap 10k) - UUID4 for call_id instead of timestamp Governance (V23, V31, V32): - Evict oldest rate limiter buckets at 100k cap - ReDoS guard: reject complex regex patterns, catch re.error - OPA URL injection: validate query path chars, URL-encode segments Remaining (V08, V09, V24, V33): - Reject unsigned plugins when verify=True - Replace unsafe eval() in docstring example - Generic 403 response (no trust score/threshold leak) - Rate-limit InMemoryTrustStore updates (10/min per agent) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 06048be commit 8708b9b

File tree

17 files changed

+160
-43
lines changed

17 files changed

+160
-43
lines changed

packages/agent-mesh/src/agentmesh/governance/opa.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,24 @@ def evaluate(self, query: str, input_data: dict) -> OPADecision:
140140
def _evaluate_remote(self, query: str, input_data: dict) -> OPADecision:
141141
"""Query a remote OPA server via REST API."""
142142
import urllib.request
143+
from urllib.parse import quote
144+
145+
# V32: Sanitise query path to prevent URL injection
146+
# Only allow alphanumeric, dots, underscores, hyphens in query
147+
import re as _re
148+
if not _re.fullmatch(r"[a-zA-Z0-9._\-]+", query):
149+
return OPADecision(
150+
allowed=False,
151+
query=query,
152+
source="remote",
153+
error=f"Invalid OPA query path: {query}",
154+
)
143155

144156
# Convert query path to URL: "data.agentmesh.allow" -> "/v1/data/agentmesh/allow"
145157
path_parts = query.replace("data.", "", 1).replace(".", "/") if query.startswith("data.") else query.replace(".", "/")
146-
url = f"{self.opa_url}/v1/data/{path_parts}"
158+
# URL-encode each segment as a safety net
159+
safe_path = "/".join(quote(seg, safe="") for seg in path_parts.split("/"))
160+
url = f"{self.opa_url}/v1/data/{safe_path}"
147161

148162
payload = json.dumps({"input": input_data}).encode("utf-8")
149163
req = urllib.request.Request(

packages/agent-mesh/src/agentmesh/governance/trust_policy.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ def _apply_operator(actual: Any, operator: ConditionOperator, expected: Any) ->
9292
elif operator == ConditionOperator.not_in:
9393
return actual not in expected
9494
elif operator == ConditionOperator.matches:
95-
return bool(re.search(str(expected), str(actual)))
95+
pattern = str(expected)
96+
# V31: Reject overly complex regex patterns to prevent ReDoS
97+
if len(pattern) > 200 or any(c in pattern for c in ['{', '(+', '(.*)*', '(.+)+']):
98+
return False
99+
try:
100+
return bool(re.search(pattern, str(actual), flags=re.DOTALL))
101+
except re.error:
102+
return False
96103
return False
97104

98105

packages/agent-mesh/src/agentmesh/identity/agent_id.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,11 @@ def verify_signature(self, data: bytes, signature: str) -> bool:
210210
signature_bytes = base64.b64decode(signature)
211211
public_key.verify(signature_bytes, data)
212212
return True
213-
except Exception:
214-
logger.debug("Signature verification failed", exc_info=True)
213+
except (ValueError, TypeError) as exc:
214+
logger.debug("Malformed key or signature data: %s", exc)
215+
return False
216+
except Exception as exc:
217+
logger.warning("Signature verification failed: %s", exc)
215218
return False
216219

217220
# Maximum delegation depth to prevent Sybil attacks via infinite chains.
@@ -296,10 +299,25 @@ def suspend(self, reason: str) -> None:
296299
self.revocation_reason = reason
297300
self.updated_at = datetime.utcnow()
298301

299-
def reactivate(self) -> None:
300-
"""Reactivate a suspended identity."""
302+
def reactivate(self, *, override_reason: bool = False) -> None:
303+
"""Reactivate a suspended identity.
304+
305+
Args:
306+
override_reason: If True, bypass the security-suspension guard.
307+
Must be explicitly set when reactivating an identity that was
308+
suspended for security reasons.
309+
"""
301310
if self.status == "revoked":
302311
raise ValueError("Cannot reactivate a revoked identity")
312+
if self.status != "suspended":
313+
raise ValueError(f"Cannot reactivate identity in '{self.status}' state")
314+
# Guard against blind reactivation of security suspensions
315+
if self.revocation_reason and "security" in self.revocation_reason.lower():
316+
if not override_reason:
317+
raise ValueError(
318+
"Identity was suspended for security reasons — "
319+
"pass override_reason=True to force reactivation"
320+
)
303321
self.status = "active"
304322
self.revocation_reason = None
305323
self.updated_at = datetime.utcnow()
@@ -314,14 +332,13 @@ def is_active(self) -> bool:
314332

315333
def has_capability(self, capability: str) -> bool:
316334
"""Check if this agent has a specific capability."""
317-
# Support wildcard matching
318335
for cap in self.capabilities:
319336
if cap == "*":
320337
return True
321338
if cap == capability:
322339
return True
323-
# Support prefix matching (e.g., "read:*" matches "read:data")
324-
if cap.endswith(":*"):
340+
# Prefix matching: "read:*" matches "read:data" but ":*" is rejected
341+
if cap.endswith(":*") and len(cap) > 2:
325342
prefix = cap[:-2]
326343
if capability.startswith(prefix + ":"):
327344
return True

packages/agent-mesh/src/agentmesh/integrations/crewai/agent.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ class InteractionRecord:
4747
class InMemoryTrustStore:
4848
"""Simple in-memory trust store for testing and development."""
4949

50+
# V33: Rate-limit trust updates to prevent inflation via rapid success spam
51+
MAX_UPDATES_PER_MINUTE = 10
52+
5053
def __init__(self, default_score: int = 500) -> None:
5154
self._scores: Dict[str, int] = {}
5255
self._default_score = default_score
56+
self._update_times: Dict[str, list] = {}
5357

5458
def get_trust_score(self, agent_did: str) -> int:
5559
return self._scores.get(agent_did, self._default_score)
@@ -58,6 +62,16 @@ def set_trust_score(self, agent_did: str, score: int) -> None:
5862
self._scores[agent_did] = max(0, min(1000, score))
5963

6064
def record_interaction(self, agent_did: str, *, success: bool) -> None:
65+
from datetime import datetime as _dt
66+
now = _dt.utcnow()
67+
# V33: Enforce rate limit on score updates
68+
times = self._update_times.setdefault(agent_did, [])
69+
cutoff = now.timestamp() - 60
70+
times[:] = [t for t in times if t > cutoff]
71+
if len(times) >= self.MAX_UPDATES_PER_MINUTE:
72+
return # silently drop — rate limited
73+
times.append(now.timestamp())
74+
6175
current = self.get_trust_score(agent_did)
6276
delta = 5 if success else -10
6377
self.set_trust_score(agent_did, current + delta)

packages/agent-mesh/src/agentmesh/integrations/django_middleware/middleware.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,7 @@ def __call__(self, request: HttpRequest) -> HttpResponse:
151151
min_score,
152152
)
153153
return JsonResponse(
154-
{
155-
"error": "Trust verification failed",
156-
"detail": (
157-
f"Agent trust score ({trust_score}) is below the "
158-
f"required threshold ({min_score})."
159-
),
160-
"agent_did": agent_did,
161-
"trust_score": trust_score,
162-
"required_score": min_score,
163-
},
154+
{"error": "Trust verification failed"},
164155
status=403,
165156
)
166157

packages/agent-mesh/src/agentmesh/integrations/langchain/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class TrustVerifiedTool(BaseTool): # type: ignore[misc]
122122
description="Performs arithmetic",
123123
agent_did="did:mesh:abc123",
124124
min_trust_score=500,
125-
inner_fn=lambda q: eval(q),
125+
inner_fn=lambda q: str(eval(q, {"__builtins__": {}}, {})), # noqa: S307 — example only; use ast.literal_eval in production
126126
)
127127
result = tool.run("2 + 2")
128128
"""

packages/agent-mesh/src/agentmesh/integrations/mcp/__init__.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from __future__ import annotations
3232

3333
import logging
34+
import uuid
3435
from dataclasses import dataclass, field
3536
from datetime import datetime, timedelta
3637
from typing import Any, Callable, Dict, List, Optional, Awaitable
@@ -115,6 +116,7 @@ def __init__(
115116
self._call_history: List[MCPToolCall] = []
116117
self._verified_clients: Dict[str, datetime] = {}
117118
self._verification_ttl = timedelta(minutes=10)
119+
self._max_verified_clients = 10_000
118120

119121
def register_tool(
120122
self,
@@ -160,6 +162,12 @@ async def verify_client(
160162
cached_time = self._verified_clients[client_did]
161163
if datetime.utcnow() - cached_time < self._verification_ttl:
162164
return True
165+
# Expired — remove stale entry
166+
del self._verified_clients[client_did]
167+
168+
# V22: Evict expired entries when cache grows too large
169+
if len(self._verified_clients) >= self._max_verified_clients:
170+
self._evict_expired_clients()
163171

164172
# Use TrustBridge if available
165173
if self.trust_bridge:
@@ -182,6 +190,16 @@ async def verify_client(
182190
logger.warning(f"Client {client_did} failed trust verification")
183191
return False
184192

193+
def _evict_expired_clients(self) -> None:
194+
"""Remove expired entries from the verified clients cache."""
195+
now = datetime.utcnow()
196+
expired = [
197+
did for did, ts in self._verified_clients.items()
198+
if now - ts >= self._verification_ttl
199+
]
200+
for did in expired:
201+
del self._verified_clients[did]
202+
185203
def _check_capability(
186204
self,
187205
client_capabilities: List[str],
@@ -224,7 +242,7 @@ async def invoke_tool(
224242
Returns:
225243
MCPToolCall with result or error
226244
"""
227-
call_id = f"{tool_name}-{datetime.utcnow().timestamp()}"
245+
call_id = f"{tool_name}-{uuid.uuid4().hex}"
228246

229247
call = MCPToolCall(
230248
call_id=call_id,
@@ -348,6 +366,21 @@ def __init__(
348366

349367
async def connect(self, server_url: str) -> bool:
350368
"""Connect to MCP server with trust verification."""
369+
# V18: Validate server URL scheme
370+
from urllib.parse import urlparse
371+
parsed = urlparse(server_url)
372+
if parsed.scheme not in ("http", "https", "ws", "wss"):
373+
logger.warning("Rejected server URL with invalid scheme: %s", server_url)
374+
return False
375+
if not parsed.hostname:
376+
logger.warning("Rejected server URL with missing host: %s", server_url)
377+
return False
378+
# Block common internal/loopback targets
379+
_blocked_hosts = {"localhost", "127.0.0.1", "::1", "0.0.0.0", "169.254.169.254"}
380+
if parsed.hostname.lower() in _blocked_hosts:
381+
logger.warning("Rejected internal/loopback server URL: %s", server_url)
382+
return False
383+
351384
# Verify server identity if TrustBridge available
352385
if self.trust_bridge:
353386
# Extract server DID from URL or discovery

packages/agent-mesh/src/agentmesh/marketplace/installer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,17 @@ def install(
9696
manifest = self._registry.get_plugin(name, version)
9797

9898
# Signature verification (first check)
99-
if verify and manifest.signature and manifest.author in self._trusted_keys:
99+
if verify:
100+
if not manifest.signature:
101+
raise MarketplaceError(
102+
f"Plugin {name}@{manifest.version} has no signature; "
103+
"install with verify=False to bypass (not recommended)"
104+
)
105+
if manifest.author not in self._trusted_keys:
106+
raise MarketplaceError(
107+
f"Plugin {name}@{manifest.version} signed by untrusted "
108+
f"author '{manifest.author}'"
109+
)
100110
public_key = self._trusted_keys[manifest.author]
101111
verify_signature(manifest, public_key)
102112
logger.info("Signature verified for %s@%s", name, manifest.version)
@@ -107,7 +117,7 @@ def install(
107117
self._resolve_dependencies(manifest, _seen=_seen)
108118

109119
# V29: Re-verify signature after dependency resolution (TOCTOU guard)
110-
if verify and manifest.signature and manifest.author in self._trusted_keys:
120+
if verify:
111121
public_key = self._trusted_keys[manifest.author]
112122
verify_signature(manifest, public_key)
113123

packages/agent-mesh/src/agentmesh/services/rate_limiter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def __init__(
108108
per_agent_rate: float = 10,
109109
per_agent_capacity: int = 20,
110110
backpressure_threshold: float = 0.8,
111+
max_agent_buckets: int = 100_000,
111112
) -> None:
112113
self._global_bucket = TokenBucket(rate=global_rate, capacity=global_capacity)
113114
self._per_agent_rate = per_agent_rate
@@ -117,11 +118,16 @@ def __init__(
117118
self._global_capacity = global_capacity
118119
self._per_agent_capacity_val = per_agent_capacity
119120
self._backpressure_threshold = backpressure_threshold
121+
self._max_agent_buckets = max_agent_buckets
120122

121123
def _get_agent_bucket(self, agent_did: str) -> TokenBucket:
122124
"""Get or create a per-agent token bucket."""
123125
with self._lock:
124126
if agent_did not in self._agent_buckets:
127+
# V23: Evict oldest buckets when limit reached
128+
if len(self._agent_buckets) >= self._max_agent_buckets:
129+
oldest_key = next(iter(self._agent_buckets))
130+
del self._agent_buckets[oldest_key]
125131
self._agent_buckets[agent_did] = TokenBucket(
126132
rate=self._per_agent_rate,
127133
capacity=self._per_agent_capacity,

packages/agent-mesh/src/agentmesh/transport/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class TransportConfig:
3737

3838
host: str = "localhost"
3939
port: int = 8080
40-
use_tls: bool = False
40+
use_tls: bool = True
4141
timeout_seconds: int = 30
4242
max_retries: int = 5
4343
retry_delay_seconds: float = 1.0

0 commit comments

Comments
 (0)