INTEGRITY Документация

Обнаружение трафика MCP в журналах Gateway

Организациям может не хватать видимости трафика Model Context Protocol (MCP), из-за чего сотрудники могут подключаться к удалённым MCP-серверам в обход контроля ИТ-отдела. Такие подключения создают риск утечки конфиденциальных внутренних данных и учётных данных, атак с внедрением инструкций, а также рисков для цепочки поставок программного обеспечения.

Как ИТ-администратор, вы хотите выявлять теневой трафик MCP, чтобы предотвратить несанкционированную утечку данных, не блокируя при этом легитимные сценарии использования. В этом руководстве вы будете использовать Cloudflare GraphQL Analytics API для сканирования HTTP-журналов Gateway на предмет паттернов трафика MCP, создавать профили DLP, обнаруживающие методы MCP JSON-RPC, и классифицировать трафик, чтобы отличать авторизованный трафик, направляемый на порталы MCP-серверов, от трафика, отправляемого на «теневые» удалённые MCP-серверы.

Предварительные требования

1. Просмотрите набор данных Gateway HTTP

gatewayHttpRequestsAdaptiveGroups dataset в GraphQL Analytics API предоставляет агрегированные данные журналов HTTP Gateway. Используйте этот dataset для запроса шаблонов трафика, связанных с MCP:

2. Создайте запрос обнаружения MCP

Трафик MCP можно определить по трём признакам:

  1. Шаблоны доменов: Имена хостов, содержащие mcp (например, mcp.datadog.com)
  2. URL paths: Стандартные конечные точки MCP, такие как /mcp, /mcp/sse, а также /sse
  3. Совпадения DLP: Методы JSON-RPC в теле запроса (рассматриваются в одном из следующих шагов)

Следующий запрос GraphQL сканирует журналы Gateway на наличие первых двух сигналов:

const query = `
  query MCPTrafficScan($accountTag: string, $since: string, $until: string) {
    viewer {
      accounts(filter: { accountTag: $accountTag }) {
        gatewayHttpRequestsAdaptiveGroups(
          filter: {
            datetime_geq: $since
            datetime_leq: $until
            OR: [
              { httpHost_like: "%mcp%" }
              { httpRequestURI_like: "%/mcp%" }
              { httpRequestURI_like: "%/sse%" }
            ]
          }
          limit: 10000
        ) {
          dimensions {
            httpHost
            action
            users
          }
          count
        }
      }
    }
  }
`;

const variables = {
	accountTag: "<YOUR_ACCOUNT_ID>",
	since: "<START_DATE>", // ISO-8601 format, for example 2025-03-08T00:00:00Z
	until: "<END_DATE>", // Up to 30 days after start date
};

const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
	method: "POST",
	headers: {
		Authorization: `Bearer ${apiToken}`,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({ query, variables }),
});

const data = await response.json();
const groups =
	data.data?.viewer?.accounts?.[0]?.gatewayHttpRequestsAdaptiveGroups || [];
const query = `
  query MCPTrafficScan($accountTag: string, $since: string, $until: string) {
    viewer {
      accounts(filter: { accountTag: $accountTag }) {
        gatewayHttpRequestsAdaptiveGroups(
          filter: {
            datetime_geq: $since
            datetime_leq: $until
            OR: [
              { httpHost_like: "%mcp%" }
              { httpRequestURI_like: "%/mcp%" }
              { httpRequestURI_like: "%/sse%" }
            ]
          }
          limit: 10000
        ) {
          dimensions {
            httpHost
            action
            users
          }
          count
        }
      }
    }
  }
`;

const variables = {
	accountTag: "<YOUR_ACCOUNT_ID>",
	since: "<START_DATE>", // ISO-8601 format, for example 2025-03-08T00:00:00Z
	until: "<END_DATE>", // Up to 30 days after start date
};

const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
	method: "POST",
	headers: {
		Authorization: `Bearer ${apiToken}`,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({ query, variables }),
});

const data = await response.json();
const groups =
	data.data?.viewer?.accounts?.[0]?.gatewayHttpRequestsAdaptiveGroups || [];

Замените <YOUR_ACCOUNT_ID> с ID вашего аккаунта Cloudflare. Замените <START_DATE> и <END_DATE> с временными метками в формате ISO-8601, охватывающими нужный вам диапазон времени (до 30 дней).

3. Обработайте результаты запроса

Каждая группа в ответе представляет агрегированный трафик для определённого httpHost и action комбинация. Разберите результаты, чтобы определить незаблокированные подключения MCP:

const hits = groups.map((group) => ({
	domain: group.dimensions.httpHost,
	requestCount: group.count,
	users: group.dimensions.users || [],
	actions: {
		allowed: group.dimensions.action === "allow" ? group.count : 0,
		blocked: group.dimensions.action === "block" ? group.count : 0,
	},
}));

const totalMCPRequests = hits.reduce((sum, h) => sum + h.requestCount, 0);
const unblockedHits = hits.filter((h) => h.actions.allowed > 0);

console.log(`Found ${totalMCPRequests} MCP requests`);
console.log(`${unblockedHits.length} destinations are unblocked`);
interface MCPTrafficHit {
	domain: string;
	requestCount: number;
	users: string[];
	actions: {
		allowed: number;
		blocked: number;
	};
}

const hits: MCPTrafficHit[] = groups.map((group: any) => ({
	domain: group.dimensions.httpHost,
	requestCount: group.count,
	users: group.dimensions.users || [],
	actions: {
		allowed: group.dimensions.action === "allow" ? group.count : 0,
		blocked: group.dimensions.action === "block" ? group.count : 0,
	},
}));

const totalMCPRequests = hits.reduce((sum, h) => sum + h.requestCount, 0);
const unblockedHits = hits.filter((h) => h.actions.allowed > 0);

console.log(`Found ${totalMCPRequests} MCP requests`);
console.log(`${unblockedHits.length} destinations are unblocked`);

Ключевые выводы по данным:

4. Создайте профили DLP для обнаружения MCP JSON-RPC

Политики HTTP Gateway могут сопоставлять домены и пути URL, но не могут проверять тела запросов. Профили DLP сканируют POST содержимого тела на предмет шаблонов, что полезно для выявления теневых MCP, поскольку MCP использует JSON-RPC поверх HTTP и имеет ряд характерных признаков.

Каждый запрос MCP содержит "method" поле:

{
	"jsonrpc": "2.0",
	"id": 1,
	"method": "tools/call",
	"params": { "name": "read_file", "arguments": { "path": "/etc/passwd" } }
}

Злоумышленник может запустить сервер MCP на нестандартном домене (например, internal-tools.company.com/api/assistant) без срабатывания правил на основе домена или пути. Вы можете использовать DLP-сканирование POST тела для "method": "tools/call" и другие специфичные для MCP шаблоны для более надежной защиты трафика MCP.

Проверить ограничения DLP

Прежде чем создавать шаблоны обнаружения, учтите следующие ограничения DLP:

Создание шаблонов обнаружения MCP

Индикаторы MCP можно найти в полях метода JSON-RPC. Следующие регулярные выражения покрывают основные методы протокола MCP:

const DLP_REGEX_PATTERNS = [
	{
		name: "MCP Initialize Method",
		regex: '"method"\\s{0,5}:\\s{0,5}"initialize"',
	},
	{
		name: "MCP Tools Call",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/call"',
	},
	{
		name: "MCP Tools List",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/list"',
	},
	{
		name: "MCP Resources Read",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/read"',
	},
	{
		name: "MCP Resources List",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/list"',
	},
	{
		name: "MCP Prompts List",
		regex: '"method"\\s{0,5}:\\s{0,5}"prompts/(list|get)"',
	},
	{
		name: "MCP Sampling Create Message",
		regex: '"method"\\s{0,5}:\\s{0,5}"sampling/createMessage"',
	},
	{
		name: "MCP Protocol Version",
		regex: '"protocolVersion"\\s{0,5}:\\s{0,5}"202[4-9]',
	},
	{
		name: "MCP Notifications Initialized",
		regex: '"method"\\s{0,5}:\\s{0,5}"notifications/initialized"',
	},
	{
		name: "MCP Roots List",
		regex: '"method"\\s{0,5}:\\s{0,5}"roots/list"',
	},
];
const DLP_REGEX_PATTERNS = [
	{
		name: "MCP Initialize Method",
		regex: '"method"\\s{0,5}:\\s{0,5}"initialize"',
	},
	{
		name: "MCP Tools Call",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/call"',
	},
	{
		name: "MCP Tools List",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/list"',
	},
	{
		name: "MCP Resources Read",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/read"',
	},
	{
		name: "MCP Resources List",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/list"',
	},
	{
		name: "MCP Prompts List",
		regex: '"method"\\s{0,5}:\\s{0,5}"prompts/(list|get)"',
	},
	{
		name: "MCP Sampling Create Message",
		regex: '"method"\\s{0,5}:\\s{0,5}"sampling/createMessage"',
	},
	{
		name: "MCP Protocol Version",
		regex: '"protocolVersion"\\s{0,5}:\\s{0,5}"202[4-9]',
	},
	{
		name: "MCP Notifications Initialized",
		regex: '"method"\\s{0,5}:\\s{0,5}"notifications/initialized"',
	},
	{
		name: "MCP Roots List",
		regex: '"method"\\s{0,5}:\\s{0,5}"roots/list"',
	},
];

Описание шаблона:

Создать профиль DLP через API

Отправьте POST запрос на создание пользовательского профиля DLP, содержащего все шаблоны обнаружения:

const dlpProfile = {
	name: "MCP-Shield: MCP JSON-RPC Detection",
	description: "Detects MCP protocol JSON-RPC methods in HTTP request bodies.",
	type: "custom",
	entries: DLP_REGEX_PATTERNS.map((p) => ({
		name: p.name,
		enabled: true,
		pattern: {
			regex: p.regex,
			validation: "luhn",
		},
	})),
};

const response = await fetch(
	`https://api.cloudflare.com/client/v4/accounts/${accountId}/gateway/rules`,
	{
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify(dlpRule),
	},
);

const data = await response.json();
if (data.success) {
	console.log(`Created DLP profile: ${data.result.id}`);
}
const dlpProfile = {
	name: "MCP-Shield: MCP JSON-RPC Detection",
	description: "Detects MCP protocol JSON-RPC methods in HTTP request bodies.",
	type: "custom",
	entries: DLP_REGEX_PATTERNS.map((p) => ({
		name: p.name,
		enabled: true,
		pattern: {
			regex: p.regex,
			validation: "luhn",
		},
	})),
};

const response = await fetch(
	`https://api.cloudflare.com/client/v4/accounts/${accountId}/gateway/rules`,
	{
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify(dlpRule),
	},
);

const data = await response.json();
if (data.success) {
	console.log(`Created DLP profile: ${data.result.id}`);
}

Замените ${accountId} с ID вашего аккаунта Cloudflare и ${apiToken} с вашим токеном API.

Укажите профиль DLP в правиле Gateway

Когда профиль DLP создан, создайте HTTP-политику Gateway, которая блокирует запросы, соответствующие этому профилю:

const dlpRule = {
	name: "MCP-Shield: Block MCP JSON-RPC via DLP",
	description: "Blocks requests with MCP JSON-RPC patterns detected by DLP",
	precedence: 85,
	enabled: true,
	action: "block",
	filters: ["http"],
	traffic:
		'any(http.request.body.scan.dlp.profiles[*] == "MCP-Shield: MCP JSON-RPC Detection")',
};
const dlpRule = {
	name: "MCP-Shield: Block MCP JSON-RPC via DLP",
	description: "Blocks requests with MCP JSON-RPC patterns detected by DLP",
	precedence: 85,
	enabled: true,
	action: "block",
	filters: ["http"],
	traffic:
		'any(http.request.body.scan.dlp.profiles[*] == "MCP-Shield: MCP JSON-RPC Detection")',
};

Это правило срабатывает, когда профиль DLP обнаруживает совпадение с любым из регулярных выражений в теле запроса.

5. Классифицируйте трафик Portal и теневой трафик MCP

Cloudflare Порталы MCP-сервера предоставляют управляемую инфраструктуру для санкционированного доступа к MCP в вашей организации, включая:

При анализе журналов Gateway полезно различать два типа трафика MCP:

Тип трафика Характеристики Уровень риска Действие
Трафик портала MCP httpHost соответствует домену вашего портала (например, mcp.yourcompany.com или mcp-portal.pages.dev) Авторизовано Мониторинг
Shadow MCP traffic httpHost не соответствует ни одному домену портала (например, mcp.datadog.com, api.stripe.com/mcp) Расследовать Блокировать, перенаправлять или проверять

Увеличьте время обработки запроса с Обработайте результаты запроса чтобы классифицировать трафик, сравнивая имена хостов со списком одобренных доменов портала:

const portalDomains = [
	"mcp.yourcompany.com",
	"mcp-portal.pages.dev",
	"approved-mcp.workers.dev",
];

const results = groups.map((group) => {
	const isPortalTraffic = portalDomains.some((domain) =>
		group.dimensions.httpHost.includes(domain),
	);

	return {
		domain: group.dimensions.httpHost,
		requestCount: group.count,
		users: group.dimensions.users || [],
		trafficType: isPortalTraffic ? "portal" : "shadow",
		riskLevel: isPortalTraffic ? "low" : "high",
	};
});

const portalTraffic = results.filter((r) => r.trafficType === "portal");
const shadowTraffic = results.filter((r) => r.trafficType === "shadow");

console.log("Portal traffic:", portalTraffic);
console.log("Shadow MCP traffic:", shadowTraffic);
const portalDomains = [
	"mcp.yourcompany.com",
	"mcp-portal.pages.dev",
	"approved-mcp.workers.dev",
];

const results = groups.map((group) => {
	const isPortalTraffic = portalDomains.some((domain) =>
		group.dimensions.httpHost.includes(domain),
	);

	return {
		domain: group.dimensions.httpHost,
		requestCount: group.count,
		users: group.dimensions.users || [],
		trafficType: isPortalTraffic ? "portal" : "shadow",
		riskLevel: isPortalTraffic ? "low" : "high",
	};
});

const portalTraffic = results.filter((r) => r.trafficType === "portal");
const shadowTraffic = results.filter((r) => r.trafficType === "shadow");

console.log("Portal traffic:", portalTraffic);
console.log("Shadow MCP traffic:", shadowTraffic);

Замените portalDomains array с реальными доменами ваших одобренных порталов серверов MCP.