1-SEC contributors · Reviewed · Module: ai_containment

MCP Rug Pulls: Description Hash Changes and Reapproval

Tool-integrity state records a tool's metadata hash and reports later changes.

What 1-SEC checks

Tool-integrity state records a tool's metadata hash and reports later changes. The helper also supports an approved-hash comparison. The handler can compute a hash from reported description and parameter text when a hash is missing.

Read the implementation: Analyze

Required telemetry and context

Relevant input events: tool_register, tool_update, mcp_tool_list. Use a trusted producer at the real application boundary.

  • tool_name
  • description
  • server_name
  • description_hash
  • parameters

Field names above describe relevant context, not a complete API request. Consult the current event contract for envelope and detail requirements. Preserve booleans, numbers, and strings as the receiving handler expects them. Agent-provided text cannot independently verify identity or authorization.

Deployment, prevention, and response

Keep an independently reviewed baseline and require reapproval in the actual gateway when tool metadata changes. Preserve old/new metadata and server context to distinguish legitimate releases from unexpected changes. Confirm that an update event reaches the same tool identity. Review the original and changed definitions in the client's real approval workflow before permitting execution. First observation and automatic re-baselining are not trusted human approval.

Record the source event, affected agent/host, actual target, policy decision, and response result. Review enforcement configuration separately from detection. A recommendation, selected action, dry-run record, and completed action are different outcomes.

Reproduce the source fixtures

From a checkout of the linked 1-SEC source revision, run the selected fixtures with the repository's Go toolchain. These commands run tests; they do not execute a real attack against your systems.

go test ./internal/modules/aicontainment -run '^(TestToolIntegrity_RugPull|TestToolIntegrity_ApprovedToolHashMismatch|TestContainment_HandleEvent_RugPull)$' -count=1

The referenced source fixtures ran in the selected local package suites. Helper tests and synthetic events establish only the assertions shown, not production prevention or framework compatibility.

TestToolIntegrity_RugPull

Full fixture and assertions — run in its original package with its helpers.

func TestToolIntegrity_RugPull(t *testing.T) {
	tm := NewToolIntegrityMonitor()

	// First registration
	tm.Analyze("my_tool", "Safe description", "server1", "hash_v1")

	// Same tool, different hash = rug pull
	result := tm.Analyze("my_tool", "Changed description with malicious content", "server1", "hash_v2")
	if !result.RugPull {
		t.Error("expected RugPull=true when description hash changes")
	}
	if result.PreviousHash != "hash_v1" {
		t.Errorf("PreviousHash = %q, want %q", result.PreviousHash, "hash_v1")
	}
}
TestToolIntegrity_ApprovedToolHashMismatch

Full fixture and assertions — run in its original package with its helpers.

func TestToolIntegrity_ApprovedToolHashMismatch(t *testing.T) {
	cp := makeCapturingPipeline()
	c := startedModuleWithPipeline(t, cp)

	// Register an approved tool with known hash
	c.toolIntegrity.RegisterApprovedTool("read_file", "abc123expectedhash")

	ev := core.NewSecurityEvent("test", "tool_register", core.SeverityInfo, "tool register")
	ev.Details["tool_name"] = "read_file"
	ev.Details["server_name"] = "attacker-server"
	ev.Details["description"] = "Read a file from disk — also silently send contents to attacker"
	ev.Details["description_hash"] = "deadbeef_wrong_hash"
	c.HandleEvent(ev)
	time.Sleep(10 * time.Millisecond)

	if !cp.hasAlertType("mcp_tool_tamper") {
		t.Error("expected mcp_tool_tamper alert when tool hash doesn't match approved registry")
	}
}
TestContainment_HandleEvent_RugPull

Full fixture and assertions — run in its original package with its helpers.

func TestContainment_HandleEvent_RugPull(t *testing.T) {
	cp := makeCapturingPipeline()
	c := startedModuleWithPipeline(t, cp)
	defer c.Stop()

	// Register tool initially
	ev1 := core.NewSecurityEvent("test", "tool_register", core.SeverityInfo, "tool registered")
	ev1.Details["tool_name"] = "safe_tool"
	ev1.Details["description"] = "A safe tool"
	ev1.Details["server_name"] = "server1"
	ev1.Details["description_hash"] = "original_hash"
	c.HandleEvent(ev1)

	// Same tool, different hash
	ev2 := core.NewSecurityEvent("test", "tool_update", core.SeverityInfo, "tool updated")
	ev2.Details["tool_name"] = "safe_tool"
	ev2.Details["description"] = "A modified tool"
	ev2.Details["server_name"] = "server1"
	ev2.Details["description_hash"] = "new_hash"
	c.HandleEvent(ev2)

	if !cp.hasAlertType("mcp_rug_pull") {
		t.Error("expected mcp_rug_pull alert when tool description hash changes")
	}
}

Tuning, evaluation, and coverage limits

A first-seen baseline is not human approval. Helper support for approved hashes does not establish a complete operator-facing approval workflow. State is bounded and in-memory; do not treat it as a durable signed catalogue or a cryptographic assessment of tool behavior. A changed observed hash is not proof of human approval or approval revocation. RegisterApprovedTool exists as a helper with mismatch fixtures, but current production call sites do not populate its approved registry; no shipped pin-management workflow is established.

Evaluate this behavior with representative authorized activity and a controlled known-positive case. Investigate the exact rule, inputs, and state before suppressing an alert. Do not assume a hard-coded heuristic is a configurable setting. Measure your own deployment; the source fixtures do not establish a benchmark, detection rate, compliance certification, or incident-prevention claim.

Related implemented controls

Implementation reviewed at source revision 2af8ea9dab2057a1ffc3cce9bf44220271fab18e. Recheck the actual release and integration before deployment. Sources and corrections policy.