Skip to content

Commit 3d297e7

Browse files
fix: address 7 HIGH security findings from deep audit
Identity (agent_id.py): - V01: Enforce MAX_DELEGATION_DEPTH=10 to prevent Sybil attacks - V02: Block wildcard '*' capability propagation via delegation Marketplace (installer.py): - V03: Path traversal guard — resolve+verify dest stays in plugins_dir - V29: TOCTOU fix — re-verify manifest signature after dep resolution Middleware: - V16: Change http_middleware permissive_mode default to False - V17: Add AGENTMESH_TRUSTED_PROXIES setting for header spoofing defense Capability (capability.py): - V30: Tighten prefix matching to require colon boundary All 1853 tests pass, 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0e6983a commit 3d297e7

File tree

7 files changed

+99
-29
lines changed

7 files changed

+99
-29
lines changed

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
from datetime import datetime
11-
from typing import Optional, Literal
11+
from typing import ClassVar, Optional, Literal
1212
from pydantic import BaseModel, Field, field_validator
1313
from cryptography.hazmat.primitives.asymmetric import ed25519
1414
from cryptography.hazmat.primitives import serialization
@@ -214,6 +214,9 @@ def verify_signature(self, data: bytes, signature: str) -> bool:
214214
logger.debug("Signature verification failed", exc_info=True)
215215
return False
216216

217+
# Maximum delegation depth to prevent Sybil attacks via infinite chains.
218+
MAX_DELEGATION_DEPTH: ClassVar[int] = 10
219+
217220
def delegate(
218221
self,
219222
name: str,
@@ -239,7 +242,25 @@ def delegate(
239242
description: Optional description.
240243
max_initial_trust_score: Upper bound on the child's initial trust
241244
score. Typically set to the parent's current trust score.
245+
246+
Raises:
247+
ValueError: If capabilities are not a subset, delegation depth
248+
exceeds MAX_DELEGATION_DEPTH, or wildcard is propagated.
242249
"""
250+
# V01: Enforce maximum delegation depth
251+
if self.delegation_depth >= self.MAX_DELEGATION_DEPTH:
252+
raise ValueError(
253+
f"Maximum delegation depth ({self.MAX_DELEGATION_DEPTH}) exceeded. "
254+
f"Current depth: {self.delegation_depth}"
255+
)
256+
257+
# V02: Block wildcard capability propagation
258+
if "*" in capabilities:
259+
raise ValueError(
260+
"Cannot delegate wildcard capability '*'. "
261+
"Explicitly list the capabilities to delegate."
262+
)
263+
243264
# Validate capabilities are a subset
244265
for cap in capabilities:
245266
if cap not in self.capabilities:

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ def _signature_header() -> str:
7070
def _exempt_paths() -> List[str]:
7171
return list(_get_setting("AGENTMESH_EXEMPT_PATHS", []))
7272

73+
@staticmethod
74+
def _trusted_proxies() -> List[str]:
75+
"""Return list of trusted proxy IPs/CIDRs.
76+
77+
When set, DID headers are only trusted from these source IPs.
78+
Empty list (default) means trust headers from any source — set
79+
this in production to prevent header spoofing.
80+
"""
81+
return list(_get_setting("AGENTMESH_TRUSTED_PROXIES", []))
82+
7383
# ------------------------------------------------------------------
7484
# request processing
7585
# ------------------------------------------------------------------
@@ -80,6 +90,20 @@ def __call__(self, request: HttpRequest) -> HttpResponse:
8090
if request.path.startswith(prefix):
8191
return self.get_response(request)
8292

93+
# V17: Validate request comes from a trusted proxy when configured
94+
trusted_proxies = self._trusted_proxies()
95+
if trusted_proxies:
96+
remote_addr = request.META.get("REMOTE_ADDR", "")
97+
if remote_addr not in trusted_proxies:
98+
logger.warning(
99+
"Rejecting agent DID header from untrusted source: %s",
100+
remote_addr,
101+
)
102+
return JsonResponse(
103+
{"error": "Untrusted proxy", "detail": "Request source is not in AGENTMESH_TRUSTED_PROXIES."},
104+
status=403,
105+
)
106+
83107
did_header = self._did_header()
84108
sig_header = self._signature_header()
85109

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class TrustConfig:
3131

3232
required_trust_score: float = 0.5
3333
required_capabilities: List[str] = field(default_factory=list)
34-
permissive_mode: bool = True # allow requests without trust headers
34+
# V16: Default to strict mode — require explicit opt-in for permissive
35+
permissive_mode: bool = False
3536

3637

3738
@dataclass

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def install(
9595
"""
9696
manifest = self._registry.get_plugin(name, version)
9797

98-
# Signature verification
98+
# Signature verification (first check)
9999
if verify and manifest.signature and manifest.author in self._trusted_keys:
100100
public_key = self._trusted_keys[manifest.author]
101101
verify_signature(manifest, public_key)
@@ -106,8 +106,19 @@ def install(
106106
_seen = set()
107107
self._resolve_dependencies(manifest, _seen=_seen)
108108

109-
# Install to plugins directory
110-
dest = self._plugins_dir / name
109+
# V29: Re-verify signature after dependency resolution (TOCTOU guard)
110+
if verify and manifest.signature and manifest.author in self._trusted_keys:
111+
public_key = self._trusted_keys[manifest.author]
112+
verify_signature(manifest, public_key)
113+
114+
# V03: Path traversal guard — ensure dest stays within plugins_dir
115+
dest = (self._plugins_dir / name).resolve()
116+
plugins_root = self._plugins_dir.resolve()
117+
if not str(dest).startswith(str(plugins_root)):
118+
raise MarketplaceError(
119+
f"Plugin name '{name}' resolves outside plugins directory "
120+
f"(path traversal blocked)"
121+
)
111122
dest.mkdir(parents=True, exist_ok=True)
112123
manifest_file = dest / MANIFEST_FILENAME
113124
import yaml

packages/agent-mesh/src/agentmesh/trust/capability.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,13 @@ def matches(self, requested: str, resource_id: Optional[str] = None) -> bool:
113113
prefix = self.capability[:-1] # e.g. "read:" from "read:*"
114114
if not requested.startswith(prefix):
115115
return False
116-
elif requested.startswith(self.capability):
117-
pass # granted is a prefix of requested
116+
elif ":" in self.capability and requested.startswith(self.capability + ":"):
117+
# V30: Only allow prefix match at colon boundaries to prevent
118+
# "read" matching "readwrite:secret". Require the granted
119+
# capability to be a colon-delimited prefix of the requested one.
120+
pass
121+
elif self.capability == requested:
122+
pass
118123
else:
119124
# Fall back to component matching
120125
req_action, req_resource, req_qualifier = self.parse_capability(requested)

packages/agent-mesh/tests/test_coverage_boost.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,19 @@ class TestTrustMiddleware:
598598
"""Tests for TrustMiddleware."""
599599

600600
def test_verify_request_permissive_no_headers(self):
601-
mw = TrustMiddleware()
601+
cfg = TrustConfig(permissive_mode=True)
602+
mw = TrustMiddleware(config=cfg)
602603
result, err = mw.verify_request({})
603604
assert result.verified is True
604605
assert err is None
605606

607+
def test_verify_request_default_strict_no_headers(self):
608+
"""V16: Default config is now strict (permissive_mode=False)."""
609+
mw = TrustMiddleware()
610+
result, err = mw.verify_request({})
611+
assert result.verified is False
612+
assert err is not None
613+
606614
def test_verify_request_strict_no_headers(self):
607615
cfg = TrustConfig(permissive_mode=False)
608616
mw = TrustMiddleware(config=cfg)

packages/agent-mesh/tests/test_negative_security.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,30 @@ def _setup_handshake():
6464
class TestDelegationDepthAbuse:
6565
"""Verify delegation depth limits are respected."""
6666

67-
def test_deep_delegation_chain(self):
68-
"""An agent can create arbitrarily deep delegation chains.
69-
This is a potential Sybil attack vector — each level spawns
70-
a new identity with a fresh keypair.
71-
"""
67+
def test_deep_delegation_chain_blocked(self):
68+
"""Delegation beyond MAX_DELEGATION_DEPTH is rejected."""
7269
root = _create_agent("root", ["read:data", "write:data"])
7370
current = root
74-
# Create a 20-level deep delegation chain
75-
for i in range(20):
71+
# Create chain up to the max (10)
72+
for i in range(AgentIdentity.MAX_DELEGATION_DEPTH):
7673
current = current.delegate(
7774
name=f"child-{i}",
7875
capabilities=["read:data"],
7976
)
80-
assert current.delegation_depth == 20
81-
# The child at depth 20 still works — no enforcement
82-
assert current.is_active()
77+
assert current.delegation_depth == AgentIdentity.MAX_DELEGATION_DEPTH
78+
# The next delegation should be rejected
79+
with pytest.raises(ValueError, match="Maximum delegation depth"):
80+
current.delegate(name="too-deep", capabilities=["read:data"])
8381

84-
def test_delegated_agent_retains_capabilities(self):
85-
"""Even at extreme depth, capabilities are preserved."""
82+
def test_delegation_at_max_depth_still_works(self):
83+
"""Delegation at depth MAX-1 succeeds (boundary check)."""
8684
root = _create_agent("root", ["read:data"])
87-
child = root
88-
for _ in range(10):
89-
child = child.delegate(name="deep", capabilities=["read:data"])
90-
assert child.has_capability("read:data")
85+
current = root
86+
for i in range(AgentIdentity.MAX_DELEGATION_DEPTH - 1):
87+
current = current.delegate(name=f"child-{i}", capabilities=["read:data"])
88+
# One more should still work (depth == MAX-1 → child depth == MAX)
89+
child = current.delegate(name="last", capabilities=["read:data"])
90+
assert child.delegation_depth == AgentIdentity.MAX_DELEGATION_DEPTH
9191

9292

9393
# ===========================================================================
@@ -110,13 +110,13 @@ def test_wildcard_parent_cannot_delegate_arbitrary_capabilities(self):
110110
capabilities=["admin:delete-all", "nuclear:launch"],
111111
)
112112

113-
def test_wildcard_parent_can_delegate_wildcard(self):
114-
"""Parent with '*' can delegate '*' itself — this is the only
115-
way to pass through the wildcard.
113+
def test_wildcard_delegation_blocked(self):
114+
"""V02: Parent with '*' CANNOT delegate '*' itself —
115+
wildcard propagation is explicitly blocked.
116116
"""
117117
root = _create_agent("root", ["*"])
118-
child = root.delegate(name="child", capabilities=["*"])
119-
assert child.has_capability("anything:at:all")
118+
with pytest.raises(ValueError, match="Cannot delegate wildcard"):
119+
root.delegate(name="child", capabilities=["*"])
120120

121121
def test_star_capability_matches_everything(self):
122122
agent = _create_agent("super", ["*"])

0 commit comments

Comments
 (0)