AID-H-034.006: MCP Server Explicit Application-Handle Isolation
Pillars: app, infra
Lifecycle: building, operation, response
Scope boundary
Owns server-side authorization, expiry, revocation, and partitioning of explicit opaque application handles passed as ordinary arguments. It does not create sessions or own task, subscription, budget, telemetry, or disable-state lifecycles.
AID-H-034.007: AID-H-034.006 authorizes references to application objects across calls; AID-H-034.007 owns durable task and subscription creation, lifecycle, terminal state, cancellation, and filtered delivery. A task may be referenced by a handle, but its lifecycle is not generic handle state.
AID-H-028.005: AID-H-034.006 authorizes the object represented by a handle on the server; AID-H-028.005 isolates and expires client-side handle references and request state. Client storage discipline cannot grant server object access.
AID-H-018.002: AID-H-034.006 owns MCP-specific cross-call handle issuance, binding, expiry, revocation, and per-use authorization; AID-H-018.002 authorizes opaque application or service objects at non-MCP model- or agent-triggered boundaries. If an MCP handler calls an external service, preserve both enforcement boundaries but award no duplicate PASS for that service's object decision.
Implementation guidance
- AID-H-034.006-G001: Represent cross-call application state with opaque, expiring, independently revocable handles and authorize the referenced object on every use.
AID-H-034.006-G001
Concept
A basket_id or browser_id is an untrusted tool argument, not authentication and not an MCP session. Store only a high-entropy handle at the client; retain its digest and owner binding server-side. Revalidate authenticated identity and the referenced object's authorization on every call.
-- File: migrations/20260728_mcp_state_handles.sql
CREATE TABLE mcp_state_handles (
handle_sha256 bytea PRIMARY KEY,
subject_key bytea NOT NULL,
tenant_id text NOT NULL,
client_id text NOT NULL,
object_type text NOT NULL,
object_ref text NOT NULL,
object_version text NOT NULL,
scopes text[] NOT NULL CHECK (cardinality(scopes) > 0),
policy_version text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
revoked_at timestamptz,
revocation_reason text
);
CREATE INDEX mcp_state_handles_owner_idx
ON mcp_state_handles (subject_key, tenant_id, client_id, expires_at);// File: src/state/applicationHandleStore.js
import crypto from "node:crypto";
const SUBJECT_KEY = Buffer.from(process.env.MCP_SUBJECT_HMAC_KEY_B64, "base64");
if (SUBJECT_KEY.length !== 32) throw new Error("MCP_SUBJECT_HMAC_KEY_B64 must decode to 32 bytes");
const handleDigest = value => crypto.createHash("sha256").update(value).digest();
const subjectKey = value => crypto.createHmac("sha256", SUBJECT_KEY).update(value).digest();
function validateContext(input) {
if (!input.auth?.subject || !input.auth.activeTenantId || !input.clientId) {
throw new Error("missing_handle_identity");
}
if (!input.policy?.version || !Number.isSafeInteger(input.policy.handleTtlMs) ||
input.policy.handleTtlMs <= 0 || !Number.isSafeInteger(input.policy.maxScopes) ||
input.policy.maxScopes <= 0 || !Array.isArray(input.policy.allowedScopes) ||
input.policy.allowedScopes.length !== new Set(input.policy.allowedScopes).size ||
!Array.isArray(input.policy.allowedObjectTypes)) {
throw new Error("invalid_signed_handle_policy");
}
}
export async function mintHandle(db, input) {
validateContext(input);
const allowedScopes = new Set(input.policy.allowedScopes || []);
const scopes = [...new Set(input.scopes)].sort();
if (!input.policy.allowedObjectTypes?.includes(input.objectType) || !input.objectRef ||
!input.objectVersion || !scopes.length || scopes.length > input.policy.maxScopes ||
scopes.some(scope => !allowedScopes.has(scope))) throw new Error("handle_grant_outside_policy");
await input.authorizeObject({ ...input, scopes, phase: "mint" });
const handle = crypto.randomBytes(32).toString("base64url");
await db.query(
"INSERT INTO mcp_state_handles " +
"(handle_sha256,subject_key,tenant_id,client_id,object_type,object_ref,object_version," +
"scopes,policy_version,expires_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9," +
"now()+($10::bigint*interval '1 millisecond'))",
[handleDigest(handle), subjectKey(input.auth.subject), input.auth.activeTenantId,
input.clientId, input.objectType, input.objectRef, input.objectVersion, scopes,
input.policy.version, input.policy.handleTtlMs]
);
return handle;
}
export async function resolveHandle(db, input) {
validateContext(input);
if (typeof input.requiredScope !== "string" || !input.requiredScope ||
!input.policy.allowedScopes.includes(input.requiredScope)) {
throw new Error("invalid_required_handle_scope");
}
const result = await db.query(
"SELECT object_type,object_ref,object_version,scopes,policy_version FROM mcp_state_handles " +
"WHERE handle_sha256=$1 AND subject_key=$2 AND tenant_id=$3 AND client_id=$4 " +
"AND revoked_at IS NULL AND expires_at>now()",
[handleDigest(input.handle), subjectKey(input.auth.subject),
input.auth.activeTenantId, input.clientId]
);
const record = result.rows[0];
if (!record || record.policy_version !== input.policy.version ||
!record.scopes.includes(input.requiredScope)) {
throw new Error("handle_not_found_or_policy_changed");
}
const currentObject = await input.loadObject({
tenantId: input.auth.activeTenantId,
objectType: record.object_type,
objectRef: record.object_ref
});
if (!currentObject || String(currentObject.version) !== record.object_version) {
throw new Error("handle_object_version_changed");
}
await input.authorizeObject({
auth: input.auth, clientId: input.clientId, object: currentObject,
requiredScope: input.requiredScope, policyVersion: record.policy_version, phase: "resolve"
});
return Object.freeze({ ...record, currentObject });
}
export async function revokeHandle(db, input) {
validateContext(input);
if (!input.reason || !input.operatorReceiptId) throw new Error("handle_revocation_evidence_required");
const result = await db.query(
"UPDATE mcp_state_handles SET revoked_at=COALESCE(revoked_at,now()),revocation_reason=$5 " +
"WHERE handle_sha256=$1 AND subject_key=$2 AND tenant_id=$3 AND client_id=$4 " +
"RETURNING revoked_at",
[handleDigest(input.handle), subjectKey(input.auth.subject),
input.auth.activeTenantId, input.clientId, input.reason]
);
if (result.rowCount !== 1) throw new Error("handle_not_found");
return { revokedAt: result.rows[0].revoked_at, operatorReceiptId: input.operatorReceiptId };
}Action and evidence: revoke on logout, token/role change, tenant switch, object deletion, suspicious use, or policy expiry. Record only handle/object digests, authenticated partition, required scope, decision, policy version, and trace ID. Cross-owner, expired, and revoked-handle tests must fail before backend work.
Verify safely
A separately credentialed verifier tests guessing, substitution across subject/client/tenant, expiry boundaries, scope escalation, object/version change, revocation race, concurrent use, store outage and database uniqueness while reconciling all lifecycle events.
Open-source tools
- Valkey
- Redis 8+ (AGPL-3.0)
- Open Policy Agent (OPA)
Commercial / hosted tools
- Redis Cloud
- Amazon ElastiCache for Valkey
- Google Cloud Memorystore for Valkey
- Azure Managed Redis
Threat mappings
MITRE ATLAS
- AML.T0086 Exfiltration via AI Agent Tool Invocation
- AML.T0012 Valid Accounts
MAESTRO
- Agent Identity Attack (L7)
- Data Exfiltration (L2)
OWASP LLM Top 10 2026
- LLM02:2026 Sensitive Information Disclosure
- LLM03:2026 Excessive Agency
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI03:2026 Identity and Privilege Abuse
NIST Adversarial Machine Learning 2025
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.1 Parameter Manipulation
- AISubtech-14.1.2 Insufficient Access Controls
- AISubtech-14.2.1 Permission Escalation via Delegation
- AISubtech-2.1.2 Capability and Permission Overreach
- AISubtech-4.2.2 Session Boundary Violation
- AISubtech-4.3.4 Replay Exploitation (rejects stale, revoked or cross-context handle reuse; valid repeat use is allowed)
- AITech-12.1 Tool Exploitation
- AITech-14.2 Abuse of Delegated Authority
- AITech-2.1 Excessive Agency
- AITech-4.2 Context Boundary Attacks
- AITech-4.3 Protocol Manipulation (rejects stale, revoked or cross-context handle reuse; valid repeat use is allowed)
- AITech-8.2 Data Exfiltration / Exposure
- AITech-8.3 Information Disclosure
Google Secure AI Framework 2.0 - Risks
- SDD: Sensitive Data Disclosure
- IIC: Insecure Integrated Component
- RA: Rogue Actions
Databricks AI Security Framework 3.0
- Agents - Tools MCP Server 13.19: Credential and Token Exposure
- Agents - Tools MCP Server 13.22: Excessive Permissions and Scope Creep
- Agents - Tools MCP Server 13.23: Data Exfiltration
- Agents - Tools MCP Server 13.24: Context Spoofing and Manipulation