AID-H-034.009: MCP Server Authoritative Disable-State & Commit-Fence Integration

Version 1.20261015 · Actionable sub-technique

When a complete signed operation registry marks an MCP operation as privileged, state-changing, or high impact, query the AID-I-005 authority online before dispatch and immediately before the protected effect. Bind each signed current-state response to a fresh server nonce, exact target, enforcement phase, policy generation, monotonic head generation, and ENABLE or DISABLE state so a cached signed ENABLE cannot masquerade as current. For effects exposed to a post-check race, require an authority-issued commit fence that the backend atomically redeems and rejects after a newer DISABLE.

Pillars: app, infra

Lifecycle: operation, response

Scope boundary

Owns complete MCP operation classification, nonce-bound current-head verification, fail-closed pre-dispatch and pre-effect checks, and delivery of authority-issued commit fences into covered backend effects.

AID-I-005: AID-H-034.009 enforces the current signed HALT or RUN state and commit fence at MCP handlers; AID-I-005 owns the incident decision, authorization, authoritative state, propagation, and fence invalidation. The MCP server consumes rather than creates emergency authority.

AID-E-001: AID-H-034.009 blocks covered MCP operations under authoritative disable state; AID-E-001 revokes compromised credentials, sessions, principals, and grants. An operation halt does not invalidate the credential objects that enabled prior access.

AID-E-002: AID-H-034.009 prevents new protected effects at the MCP server; AID-E-002 terminates compromised runtime processes, leases, and pods. Commit fencing and workload eviction address different residual execution paths.

Implementation guidance

AID-H-034.009-G001

Verify a nonce-bound authoritative current checkpoint before MCP dispatch and effect, reject head rollback, and carry revocable authority fences into each covered backend commit.
Applicability and ownership

Use this integration only for an MCP operation that the complete, locally signature-verified operation registry classifies as COVERED. The MCP server owns policy classification, current-state verification, pre-dispatch and pre-effect enforcement, and delivery of commit fences. AID-I-005 owns the decision and authoritative state. A registry entry may be NOT_APPLICABLE only with a technical architecture reason; an unlisted operation is an error and never inherits an allow.

Verify the policy and online authority response locally

The authority signs each response over a fresh server-generated query nonce, exact target, enforcement phase, policy generation, and current head generation. This prevents a cached signed ENABLE from being replayed as the latest state. Numeric freshness bounds and operation coverage come from the signed policy.

// File: src/disable/mcpDisableEnforcement.js
import crypto from "node:crypto";
import fs from "node:fs";
import https from "node:https";
import path from "node:path";

const TARGET_FIELD = Object.freeze({
  server_release: "serverRelease",
  tenant: "tenantId",
  client: "clientId",
  tool: "toolName",
  resource: "resourceId",
  task: "taskId",
  subscription: "subscriptionId",
  upstream: "upstreamId",
  object: "objectId"
});
const COVERAGE = new Set(["COVERED", "NOT_APPLICABLE"]);
const PHASES = new Set(["pre_dispatch", "pre_side_effect"]);

const exactKeys = (value, expected) =>
  value && Object.getPrototypeOf(value) === Object.prototype
  && Object.keys(value).sort().join(",")
    === [...expected].sort().join(",");

const canonical = value => {
  if (
    value === null || typeof value === "string"
    || typeof value === "boolean"
  ) return JSON.stringify(value);
  if (Number.isSafeInteger(value)) return String(value);
  if (Array.isArray(value)) {
    return "[" + value.map(canonical).join(",") + "]";
  }
  if (value && Object.getPrototypeOf(value) === Object.prototype) {
    return "{" + Object.keys(value).sort().map(
      key => JSON.stringify(key) + ":" + canonical(value[key])
    ).join(",") + "}";
  }
  throw new Error("non_canonical_disable_value");
};

const decodeBase64 = value => {
  if (typeof value !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
    throw new Error("invalid_disable_base64");
  }
  const decoded = Buffer.from(value, "base64");
  if (!decoded.length || decoded.toString("base64") !== value) {
    throw new Error("non_canonical_disable_base64");
  }
  return decoded;
};

function verifyEnvelope(rawEnvelope, publicKey) {
  if (!Buffer.isBuffer(rawEnvelope) || !rawEnvelope.length) {
    throw new Error("signed_disable_envelope_unavailable");
  }
  const envelope = JSON.parse(rawEnvelope.toString("utf8"));
  if (!exactKeys(envelope, ["payload_b64", "signature_b64"])) {
    throw new Error("disable_envelope_schema_differs");
  }
  const payloadBytes = decodeBase64(envelope.payload_b64);
  const signature = decodeBase64(envelope.signature_b64);
  if (!crypto.verify(null, payloadBytes, publicKey, signature)) {
    throw new Error("disable_signature_invalid");
  }
  return {
    payload: JSON.parse(payloadBytes.toString("utf8")),
    payloadDigest: crypto.createHash("sha256")
      .update(payloadBytes).digest("hex")
  };
}

function positiveInteger(value, field) {
  if (!Number.isSafeInteger(value) || value <= 0) {
    throw new Error("invalid_" + field);
  }
  return value;
}

function postHttps(url, options, body) {
  return new Promise((resolve, reject) => {
    const request = https.request(url, {
      method: "POST",
      agent: options.agent,
      headers: options.headers,
      timeout: options.timeoutMs
    }, response => {
      const chunks = [];
      let received = 0;
      response.on("data", chunk => {
        received += chunk.length;
        if (received > options.maxResponseBytes) {
          request.destroy(new Error("disable_authority_response_too_large"));
          return;
        }
        chunks.push(chunk);
      });
      response.on("end", () => resolve({
        statusCode: response.statusCode || 0,
        body: Buffer.concat(chunks)
      }));
    });
    request.on("timeout", () =>
      request.destroy(new Error("disable_authority_timeout"))
    );
    request.on("error", reject);
    request.end(body);
  });
}

function loadHexKey(name) {
  const encoded = process.env[name] || "";
  if (!/^[a-f0-9]+$/.test(encoded) || encoded.length % 2 !== 0) {
    throw new Error("invalid_hex_key:" + name);
  }
  const key = Buffer.from(encoded, "hex");
  if (key.length < 32) throw new Error("short_key:" + name);
  return key;
}

export function loadVerifiedDisablePolicy() {
  const rootPublicKey = crypto.createPublicKey(
    fs.readFileSync(process.env.MCP_DISABLE_POLICY_VERIFY_KEY)
  );
  const rawEnvelope = fs.readFileSync(
    process.env.MCP_DISABLE_POLICY_ENVELOPE
  );
  const verified = verifyEnvelope(rawEnvelope, rootPublicKey);
  const policy = verified.payload;
  const fields = [
    "authority", "authority_key_path", "authority_key_sha256",
    "max_authority_response_age_ms", "not_after", "operations",
    "policy_generation", "policy_version", "schema_version"
  ];
  if (
    !exactKeys(policy, fields)
    || policy.schema_version !== "aidefend.mcp-disable-policy.v2"
    || typeof policy.policy_version !== "string"
    || !policy.policy_version
    || !Number.isSafeInteger(policy.policy_generation)
    || policy.policy_generation <= 0
    || typeof policy.authority !== "string"
    || !/^[a-zA-Z0-9._:-]+$/.test(policy.authority)
    || !/^[a-f0-9]{64}$/.test(policy.authority_key_sha256)
    || !Number.isFinite(Date.parse(policy.not_after))
    || Date.parse(policy.not_after) <= Date.now()
    || !policy.operations
    || Object.getPrototypeOf(policy.operations) !== Object.prototype
    || Object.keys(policy.operations).length === 0
  ) {
    throw new Error("disable_policy_schema_or_freshness_invalid");
  }
  positiveInteger(
    policy.max_authority_response_age_ms,
    "max_authority_response_age_ms"
  );
  for (const [operation, rule] of Object.entries(policy.operations)) {
    if (
      !operation
      || !exactKeys(rule, [
        "coverage", "require_commit_fence",
        "required_target_kinds", "technical_reason"
      ])
      || !COVERAGE.has(rule.coverage)
      || typeof rule.require_commit_fence !== "boolean"
      || !Array.isArray(rule.required_target_kinds)
      || rule.required_target_kinds.length
        !== new Set(rule.required_target_kinds).size
      || rule.required_target_kinds.some(
        kind => !Object.hasOwn(TARGET_FIELD, kind)
      )
    ) {
      throw new Error("invalid_disable_operation_rule:" + operation);
    }
    if (
      rule.coverage === "COVERED"
      && (
        rule.required_target_kinds.length === 0
        || rule.technical_reason !== null
      )
    ) {
      throw new Error("covered_disable_rule_is_incomplete:" + operation);
    }
    if (
      rule.coverage === "NOT_APPLICABLE"
      && (
        rule.required_target_kinds.length !== 0
        || rule.require_commit_fence
        || typeof rule.technical_reason !== "string"
        || !rule.technical_reason.trim()
      )
    ) {
      throw new Error("invalid_disable_not_applicable_rule:" + operation);
    }
  }

  const keyRoot = path.resolve(process.env.MCP_DISABLE_AUTHORITY_KEY_ROOT);
  const keyPath = path.resolve(keyRoot, policy.authority_key_path);
  if (!keyPath.startsWith(keyRoot + path.sep)) {
    throw new Error("disable_authority_key_escapes_trust_root");
  }
  const keyBytes = fs.readFileSync(keyPath);
  const keyDigest = crypto.createHash("sha256")
    .update(keyBytes).digest("hex");
  if (keyDigest !== policy.authority_key_sha256) {
    throw new Error("disable_authority_key_digest_differs");
  }
  return Object.freeze({
    ...policy,
    policyDigest: verified.payloadDigest,
    authorityPublicKey: crypto.createPublicKey(keyBytes)
  });
}

const ACCEPT_MONOTONIC = [
  "local current=tonumber(redis.call('HGET',KEYS[1],'generation') or '0')",
  "local incoming=tonumber(ARGV[1])",
  "if not incoming or incoming<1 then return redis.error_reply('generation') end",
  "if incoming<current then return {'STALE'} end",
  "if incoming==current then",
  "  if redis.call('HGET',KEYS[1],'digest')~=ARGV[2] then return {'CONFLICT'} end",
  "  return {'REPLAY'}",
  "end",
  "redis.call('HSET',KEYS[1],'generation',incoming,'digest',ARGV[2],'state',ARGV[3])",
  "return {'ACCEPTED'}"
].join("\n");

async function acceptMonotonic(store, key, generation, digest, state) {
  const result = await store.eval(ACCEPT_MONOTONIC, {
    keys: [key],
    arguments: [String(generation), digest, state]
  });
  const status = String(result[0]);
  if (status === "STALE" || status === "CONFLICT") {
    throw new Error("disable_monotonic_" + status.toLowerCase());
  }
  if (status !== "ACCEPTED" && status !== "REPLAY") {
    throw new Error("disable_monotonic_store_error");
  }
}

export class DisableAuthorityClient {
  constructor() {
    this.baseUrl = new URL(process.env.MCP_DISABLE_AUTHORITY_URL);
    if (this.baseUrl.protocol !== "https:") {
      throw new Error("disable authority must use HTTPS");
    }
    this.timeoutMs = positiveInteger(
      Number(process.env.MCP_DISABLE_AUTHORITY_TIMEOUT_MS),
      "disable_authority_timeout_ms"
    );
    this.maxResponseBytes = positiveInteger(
      Number(process.env.MCP_DISABLE_AUTHORITY_MAX_RESPONSE_BYTES),
      "disable_authority_max_response_bytes"
    );
    this.agent = new https.Agent({
      ca: fs.readFileSync(process.env.MCP_DISABLE_AUTHORITY_CA_FILE),
      cert: fs.readFileSync(process.env.MCP_DISABLE_CLIENT_CERT),
      key: fs.readFileSync(process.env.MCP_DISABLE_CLIENT_KEY),
      minVersion: "TLSv1.2"
    });
  }

  async readCurrentHead(policy, target, phase) {
    const queryNonce = crypto.randomUUID();
    const url = new URL("/v1/disable/current-head", this.baseUrl);
    if (url.origin !== this.baseUrl.origin) {
      throw new Error("disable_authority_origin_changed");
    }
    const response = await postHttps(url, {
      agent: this.agent,
      headers: {
        "cache-control": "no-store",
        "content-type": "application/json"
      },
      timeoutMs: this.timeoutMs,
      maxResponseBytes: this.maxResponseBytes
    }, canonical({
        schema_version: "aidefend.mcp-disable-query.v1",
        policy_digest: policy.policyDigest,
        policy_generation: policy.policy_generation,
        query_nonce: queryNonce,
        phase,
        target
      }));
    const rawEnvelope = response.body;
    if (response.statusCode !== 200) {
      throw new Error("disable_authority_http_" + response.statusCode);
    }
    const verified = verifyEnvelope(
      rawEnvelope, policy.authorityPublicKey
    );
    const head = verified.payload;
    const fields = [
      "authority", "commit_token", "expires_at", "generation",
      "issued_at", "phase", "policy_digest", "policy_generation",
      "query_nonce", "reason_code", "schema_version", "state", "target"
    ];
    const now = Date.now();
    const issuedAt = Date.parse(head.issued_at);
    const expiresAt = Date.parse(head.expires_at);
    if (
      !exactKeys(head, fields)
      || !exactKeys(head.target, ["digest", "kind"])
      || head.schema_version !== "aidefend.mcp-disable-head.v2"
      || head.authority !== policy.authority
      || head.policy_digest !== policy.policyDigest
      || head.policy_generation !== policy.policy_generation
      || head.query_nonce !== queryNonce
      || head.phase !== phase
      || head.target.kind !== target.kind
      || head.target.digest !== target.digest
      || !Number.isSafeInteger(head.generation)
      || head.generation <= 0
      || !["ENABLE", "DISABLE"].includes(head.state)
      || typeof head.reason_code !== "string"
      || !head.reason_code
      || !Number.isFinite(issuedAt)
      || !Number.isFinite(expiresAt)
      || issuedAt > now
      || now - issuedAt > policy.max_authority_response_age_ms
      || expiresAt <= now
    ) {
      throw new Error("disable_authority_head_invalid");
    }
    if (
      head.commit_token !== null
      && (typeof head.commit_token !== "string" || !head.commit_token)
    ) {
      throw new Error("disable_commit_token_invalid");
    }
    // Only authority state belongs to the monotonic identity. Nonce, phase,
    // response times and one-use commit tokens remain verified envelope data.
    const stateDigest = crypto.createHash("sha256").update(canonical({
      authority: head.authority,
      target: head.target,
      generation: head.generation,
      state: head.state,
      reason_code: head.reason_code
    })).digest("hex");
    return {...head, payloadDigest: verified.payloadDigest, stateDigest};
  }
}

function operationRule(policy, operationKey) {
  if (typeof operationKey !== "string" || !operationKey) {
    throw new Error("server_derived_operation_key_missing");
  }
  const rule = policy.operations[operationKey];
  if (!rule) {
    throw new Error("operation_absent_from_complete_disable_registry");
  }
  return rule;
}

function requiredTargets(context, rule, digestKey) {
  return rule.required_target_kinds.map(kind => {
    const value = context[TARGET_FIELD[kind]];
    if (typeof value !== "string" || !value) {
      throw new Error("required_disable_target_missing:" + kind);
    }
    const digest = crypto.createHmac("sha256", digestKey)
      .update(kind).update(Buffer.from([0]))
      .update(value).digest("hex");
    return {kind, digest};
  });
}

export async function createDisableEnforcer(input) {
  const policy = loadVerifiedDisablePolicy();
  await acceptMonotonic(
    input.generationStore,
    "mcp-disable-policy:{" + policy.authority + "}",
    policy.policy_generation,
    policy.policyDigest,
    "POLICY"
  );
  const digestKey = loadHexKey("MCP_DISABLE_TARGET_HMAC_KEY_HEX");
  const client = new DisableAuthorityClient();

  return async function enforce(operationKey, context, phase) {
    if (!PHASES.has(phase)) throw new Error("invalid_disable_phase");
    if (Date.parse(policy.not_after) <= Date.now()) {
      throw new Error("disable_policy_expired");
    }
    const rule = operationRule(policy, operationKey);
    if (rule.coverage === "NOT_APPLICABLE") {
      return {
        applicable: false,
        phase,
        technicalReason: rule.technical_reason
      };
    }
    const targets = requiredTargets(context, rule, digestKey);
    const heads = [];
    for (const target of targets) {
      const head = await client.readCurrentHead(policy, target, phase);
      await acceptMonotonic(
        input.generationStore,
        "mcp-disable:" + policy.authority + ":" + target.kind + ":{" + target.digest + "}:head",
        head.generation,
        head.stateDigest,
        head.state
      );
      if (head.state === "DISABLE") {
        throw new Error("mcp_operation_disabled:" + target.kind);
      }
      if (
        phase === "pre_side_effect"
        && rule.require_commit_fence
        && !head.commit_token
      ) {
        throw new Error("required_disable_commit_fence_missing");
      }
      heads.push(head);
    }
    return {
      applicable: true,
      phase,
      requireCommitFence: rule.require_commit_fence,
      heads
    };
  };
}

export async function runWithDisableEnforcement(input) {
  const enforce = await createDisableEnforcer(input);
  await enforce(input.operationKey, input.context, "pre_dispatch");
  const prepared = await input.prepareSideEffectFree();
  const current = await enforce(
    input.operationKey, input.context, "pre_side_effect"
  );
  if (!current.applicable) {
    // The signed complete registry explicitly excludes this operation.
    // Keep the same commit adapter; its empty fence set is deliberate here.
    return input.commitWithDisableFences(prepared, []);
  }
  const commitTokens = current.heads.map(head => ({
    target: head.target,
    generation: head.generation,
    token: head.commit_token
  }));
  if (
    current.requireCommitFence
    && commitTokens.some(item => !item.token)
  ) {
    throw new Error("incomplete_disable_commit_fence_set");
  }
  return input.commitWithDisableFences(prepared, commitTokens);
}

Commit-fence contract: For a policy entry with require_commit_fence, the backend must atomically redeem every authority-issued token at the protected commit point and reject a token invalidated by a newer DISABLE generation. Merely checking in the MCP process cannot close the race after the second hook. prepareSideEffectFree must not perform network writes, queue publication, durable file changes, or other protected effects.

Verify safely

Enumerate every exposed MCP method and tool against the signed operation registry. Test an omitted operation, justified NOT_APPLICABLE entry, signed ENABLE and DISABLE, wrong policy or target, replayed nonce, stale-but-signed ENABLE, lower and conflicting generations, authority timeout, cache partition, process restart, newer re-enable, and DISABLE arriving between the second hook and backend commit. Confirm all covered failures block and the backend rejects a fence superseded before commit.

Open-source tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-034.009: MCP Server Authoritative Disable-State & Commit-Fence Integration | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version