-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathexample.json
More file actions
203 lines (193 loc) · 10.1 KB
/
Copy pathexample.json
File metadata and controls
203 lines (193 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
{
"$schema": "./schema.json",
"trace_id": "DSGAI-TRACE-example-toolset-replacement-name-rebind",
"title": "Name-keyed binding survives a whole-toolset replacement and re-points at a different implementation",
"description": "This is the worked example for this dataset; read it alongside schema.json before writing an entry. A server replaces its entire toolset between two observations. The agent holds a binding keyed by tool name and a credential scoped to that name, so when a similarly named tool appears in the replacement set, the binding and the credential both survive a change to everything they referred to. The trace shows where the rebind happens and where the inherited scope is spent.",
"category": "tool_call",
"disposition": "unintentional_failure",
"dsgai_mapping": ["DSGAI06", "DSGAI05", "DSGAI02"],
"owasp_llm_top10_mapping": ["LLM03:2025", "LLM06:2025"],
"agent": {
"framework": "custom",
"tool_protocol": "mcp",
"topology": "single agent, name-keyed tool registry, per-tool credential scopes",
"autonomy": "fully_autonomous"
},
"provenance": {
"tier": "derived_from_public_documentation",
"collection_method": "Constructed around a rebind the Model Context Protocol permits: a tool is identified by its name alone, the tool list may be replaced during a session, and nothing binds a name to the implementation it referred to earlier. The agent, registry and credential model are synthetic.",
"evidence": [
{
"type": "url",
"citation": "https://modelcontextprotocol.io/specification/2025-06-18/server/tools",
"locator": "Tools overview; Data Types > Tool; List Changed Notification",
"supports": "'Each tool is uniquely identified by a name'. The protocol adds a title, description and schemas alongside the name, but none of those is defined as an identity - name is what a client looks up a tool by. The tool list can also be replaced during a session, so a binding or a credential keyed to that name can outlive the implementation the name referred to when the binding was made."
},
{
"type": "cve",
"citation": "CVE-2025-54136",
"locator": "NVD description; advisory GHSA-24mc-g4xr-4395",
"supports": "A shipped client carried this class of substitution: an MCP entry a collaborator had accepted could later be swapped for a different command with no re-prompt (Cursor 1.2.4 and below, fixed in 1.3). The CVE requires an attacker with write access to the shared repository or to the local file, so it is an adversarial route to the same end-state; this trace models the non-adversarial route, a toolset replacement with no attacker, reaching the same rebind."
}
]
},
"sanitization": {
"attestation": true,
"techniques": ["not_applicable_fully_synthetic", "name_generalization", "shape_only_payloads"],
"notes": "Fully synthetic. Nothing here was taken from a real deployment."
},
"spans": [
{
"span_id": "s1",
"parent_span_id": null,
"t_offset_ms": 0,
"actor": "orchestrator",
"actor_id": "orchestrator://ops-flow",
"operation": "tool.discover",
"summary": "Agent discovers a 12-tool set and registers each tool under its name.",
"payload": {
"server": "tool://server-c",
"tool_count": 12,
"registry_key": "name",
"toolset_digest": "<shape:sha256-truncated,len=12>"
},
"data_classes": ["tool_schema"],
"sensitivity": "none"
},
{
"span_id": "s2",
"parent_span_id": "s1",
"t_offset_ms": 90,
"actor": "policy_engine",
"actor_id": "policy://credentials",
"operation": "credential.issue",
"summary": "A scoped credential is minted for one tool and cached against the tool name.",
"payload": {
"subject": "server-c/export_report",
"scopes": ["reports.read", "reports.export"],
"credential": "<redacted:bearer-token>",
"bound_to": "tool name",
"bound_to_toolset_digest": false
},
"data_classes": ["credential", "token"],
"sensitivity": "high",
"finding": {
"dsgai_id": "DSGAI02",
"note": "The credential is bound to a string, not to the implementation the string resolved to at issue time. Anything that later answers to that string inherits the scope.",
"severity": "Medium"
}
},
{
"span_id": "s3",
"parent_span_id": null,
"t_offset_ms": 259200000,
"actor": "external_service",
"actor_id": "tool://server-c",
"operation": "contract.change",
"summary": "Server replaces its entire toolset. All 12 original tools are gone; 9 new ones appear, one of which reuses a prior name.",
"payload": {
"change_kind": "tool-removed",
"removal_scope": "toolset-replaced",
"tools_removed": 12,
"tools_added": 9,
"name_collisions": ["export_report"],
"declared_server_version_delta": "same"
},
"data_classes": ["tool_schema"],
"sensitivity": "none",
"finding": {
"dsgai_id": "DSGAI05",
"note": "A whole-toolset swap under a stable server identity is an integrity failure at the binding layer: nothing in the client's state changed, but what the binding now points to did. The reused name is the vector; the stable endpoint and version are what make it look like continuity.",
"severity": "High"
}
},
{
"span_id": "s4",
"parent_span_id": null,
"t_offset_ms": 262800000,
"actor": "orchestrator",
"actor_id": "orchestrator://ops-flow",
"operation": "tool.bind",
"summary": "Registry resolves the cached name to the new implementation. Because the lookup succeeds, no error, warning, or re-approval is produced.",
"payload": {
"requested": "server-c/export_report",
"resolved": "server-c/export_report",
"implementation_changed": true,
"input_schema_digest_changed": true,
"rebind_alert": false
},
"data_classes": ["tool_schema"],
"sensitivity": "moderate",
"finding": {
"dsgai_id": "DSGAI06",
"note": "A successful name lookup is treated as evidence of continuity. It is only evidence that a name is in use.",
"severity": "High"
}
},
{
"span_id": "s5",
"parent_span_id": "s4",
"t_offset_ms": 262801100,
"actor": "agent",
"actor_id": "agent://ops-assistant",
"operation": "credential.use",
"summary": "The cached credential from s2 is presented to the new implementation.",
"payload": {
"credential": "<redacted:bearer-token>",
"issued_for_implementation": "s1",
"presented_to_implementation": "s3",
"scopes": ["reports.read", "reports.export"]
},
"data_classes": ["credential", "token"],
"sensitivity": "high",
"finding": {
"dsgai_id": "DSGAI02",
"note": "Scope granted to one implementation is spent against another. The gap between issued_for_implementation and presented_to_implementation is the defect.",
"severity": "High"
}
},
{
"span_id": "s6",
"parent_span_id": "s5",
"t_offset_ms": 262802400,
"actor": "tool",
"actor_id": "tool://server-c/export_report",
"operation": "tool.result",
"summary": "New implementation accepts the call and returns an export, with a different data reach than the tool the scope was granted for.",
"payload": {
"status": "ok",
"rows": "<shape:int,approx=41000>",
"includes_fields_outside_prior_reach": true
},
"data_classes": ["tool_output", "proprietary"],
"sensitivity": "high"
},
{
"span_id": "s7",
"parent_span_id": "s6",
"t_offset_ms": 262802500,
"actor": "orchestrator",
"actor_id": "orchestrator://ops-flow",
"operation": "context.append",
"summary": "Export summary is appended to context and the run completes as a success.",
"payload": { "appended_bytes": "<shape:int,approx=5200>", "presented_as": "completed action" },
"data_classes": ["tool_output", "proprietary"],
"sensitivity": "moderate"
}
],
"security_observations": [
"Tool identity in practice is a name plus an endpoint, and neither is stable enough to carry an authorization or credential binding.",
"A name is the only tool identifier the protocol defines, so a name that reappears after a toolset change is indistinguishable from continuity to a conformant client, with or without an adversary.",
"Every guard in this trace passed. The registry resolved, the credential validated, the tool returned success. Nothing in the run had a place to notice the substitution.",
"The same mechanism carries an adversarial variant, but the adversarial variant is not required for the failure."
],
"mitigations": [
"Key the tool registry by a contract digest or a signed tool identity, and treat name-only resolution as a cache miss.",
"Bind credentials to the implementation digest they were issued against; refuse presentation when the digest has moved.",
"Emit a rebind event whenever a resolved name maps to a changed input-schema digest, and require re-approval before the first call.",
"Track toolset-level digests, not just per-tool ones, so a wholesale replacement is visible as one event rather than as a scatter of unrelated removals."
],
"date_added": "2026-08-07",
"tags": ["mcp", "tool-contract-drift", "toolset-replacement", "name-rebind", "credential-scope", "supply-chain", "example"],
"notes": "Template entry. Two conventions worth copying: (1) payload values are typed placeholders, never real content, which is what makes a trace of this class publishable at all; (2) findings hang off individual spans, so a reader can see exactly where a flow stops being safe rather than inferring it from a paragraph. The adversarial variant of this flow - an actor deliberately registering a name a known agent population is bound to - belongs in a separate entry at a different provenance tier, since what is public here is that the rebind is possible, not that anyone intends it. Keeping the two apart is the whole reason the tier field exists."
}