# Безопасность MCP: OAuth, права tools и prompt injection

> Практический гайд по безопасности MCP: OAuth и audience binding, least privilege для tools, защита от prompt injection, sandbox, audit logs и тесты.
> Author: Roman Belov · Published: 2026-08-21 · Source: https://futurecraft.pro/ru/blog/mcp-security-guide/

MCP-сервер превращает вывод модели в capability. Поиск события в календаре почти
безопасен. Отправка письма, SQL-запрос, изменение cloud infrastructure или чтение
домашней директории — уже нет. Поэтому главный вопрос звучит не «безопасен ли этот
prompt?», а так:

> Если один компонент обманут или скомпрометирован, что он сможет прочитать, изменить
> или отправить?

Такое framing находит проблемы, которые OAuth не решает. Валидного пользователя можно
склонить к неверному подтверждению. Доверенный сервер может вернуть отравленный
контент. Легитимный локальный сервер может получить неоправданно широкие права.

Это security-слой для общего разбора
[архитектуры MCP](/ru/blog/mcp-servers-explained/) и
[MCP-серверов в production](/ru/blog/mcp-production-custom-servers/). Материал опирается
на актуальную спецификацию от **28 июля 2026 года**; ссылки ведут на эту версию, а не на
пересказ без даты.

## Сначала нарисуйте trust boundaries

Обычный remote path включает четыре principals:

```text
user → MCP host/client → MCP server → upstream API or database
                         ↘ model context and tool results
```

У них нет общей identity и единого уровня доверия.

- **Host** решает, какой context и tools увидит модель.
- **Client** говорит по MCP и передаёт authorization текущего caller.
- **Server** проверяет caller и исполняет tool policy.
- **Upstream system** использует собственную identity и модель прав.
- **Model** предлагает действия, но не является authorization engine.

Зафиксируйте, на какой границе проверяются identity, tenant, ownership объекта, scope и
user approval. Если в таблице написано «это контролирует prompt», жёсткого контроля там
нет.

## Threat model: какие сбои действительно проектировать

| Сбой | Во что он превращается | Основной контроль |
| --- | --- | --- |
| Вредоносный tool description | Модель выбирает чужой tool или раскрывает context | Trust registry, review, allowlist, раздельные contexts |
| Injection в документе или tool result | Read-операция управляет последующей записью | Считать result данными, разделить read и act, подтверждать опасные calls |
| Широкие scopes или wildcard tools | Один токен открывает несвязанные системы | Per-capability scopes и step-up authorization |
| Token passthrough | Токен повторно используется не тем сервисом | Audience binding и отдельные upstream credentials |
| Непроверенные arguments | SQL, command, path, URL или tenant injection | Schema, semantic validation и server-side policy |
| Угадываемый workflow handle | Доступ к состоянию другого пользователя | Случайный expiring handle, привязанный к principal |
| SSRF через OAuth metadata | Доступ к внутренним сервисам или cloud metadata | HTTPS, redirect validation и egress policy |
| Слишком мощный local server | Чтение source, SSH keys или credentials | Sandbox и явные filesystem/network grants |
| Подробная telemetry | Credentials или customer data остаются в logs | Field allowlist, redaction, access control, retention |

«Модель может hallucinate» — не threat model. Полезная угроза связывает сбой с
capability и конкретным ущербом.

## Remote authorization: защищаем resource-server boundary

Для HTTP transports MCP задаёт OAuth-based flow. Актуальная
[authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)
требует Protected Resource Metadata для поиска authorization server и Resource
Indicators, чтобы токен был привязан к нужному MCP server.

Каждый защищённый запрос проходит детерминированную цепочку:

```text
signature → issuer → audience/resource → expiry → scopes → tenant/object policy
```

Первые пять проверок валидируют credential. Последняя — само действие. Scope
`tickets:write` не доказывает, что caller может менять любой ticket в любом tenant.

### Привязывайте scopes к capabilities

Не заводите один `mcp:*` или `admin`. Используйте стабильные и узкие permissions:

```text
issues:read
issues:comment
issues:close
deployments:read
deployments:promote
```

Начинайте с discovery и read access. Более сильный scope запрашивайте при первой
привилегированной операции. Текущая спецификация поддерживает это через `403` и
`insufficient_scope`. В официальном разделе
[scope minimization](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#scope-minimization)
объясняется, почему выдача всех прав заранее увеличивает blast radius и обесценивает
consent.

### Не передавайте MCP-токен дальше

Token passthrough — не shortcut, а удаление границы между MCP server и API за ним.
Спецификация требует принимать только токены с audience этого MCP server и запрещает
пересылать чужие токены downstream.

Если сервер вызывает GitHub, Google или внутренний API, используйте отдельный upstream
token:

```text
client --token audience:mcp.example--> MCP server
MCP server --different upstream token--> API
```

Mapping хранится на сервере. Ни один токен не должен попадать в model context, tool
result, query string или штатные logs.

### OAuth discovery тоже делает исходящие запросы

Вредоносный сервер может положить внутренний адрес в resource или authorization
metadata. Валидируйте каждый discovery URL и каждый redirect hop. В production следует
требовать HTTPS, запрещать private, loopback, link-local и cloud metadata ranges, кроме
явного локального исключения, и повторять проверку после DNS resolution. Egress proxy
надёжнее самописного списка строк.

Официальный раздел про
[SSRF](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#server-side-request-forgery-ssrf)
разбирает DNS rebinding и redirect chains, которые пропускает наивная URL validation.

## Authorization не заменяет подтверждение tool call

Authorization отвечает: **может ли principal использовать capability?** Approval:
**нужно ли сейчас выполнить это действие с этими аргументами?** Часто нужны оба слоя.

| Операция | Default handling |
| --- | --- |
| Чтение из узкого доверенного источника | Разрешить после обычной authorization |
| Web, email или uploaded files | Разрешить чтение, пометить result как untrusted |
| Создание обратимого draft | Показать target и важные fields |
| Send, publish, charge, delete, deploy, смена access | Запросить явное подтверждение |
| Arbitrary code или query | Запретить по умолчанию; дать узкую операцию |

MCP tools specification рекомендует оставлять человеку возможность отклонить call и
показывать inputs чувствительной операции. Tool annotations помогают UI, но не
исполняют policy. Согласно
[спецификации tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools),
клиент обязан считать annotations недоверенными, пока не доверяет самому серверу.

Approval должен показывать последствие, а не непрозрачный JSON. «Отправить сообщение
alex@example.com» понятно. «Запустить `messages.execute`?» — нет. Привяжите
подтверждение к canonical form аргументов, чтобы target нельзя было подменить между
preview и execution.

## Любой model-facing content считается недоверенным

Prompt injection часто приходит косвенно: загруженная страница, issue description,
email, комментарий в source или MCP tool result предлагает модели забыть исходную
задачу и выполнить другую. Источником может быть операционно доверенный сервер, который
читает недоверенные данные.

Не оставляйте system prompt единственной защитой. Практические контроли стоят вокруг
модели:

1. **Разделяйте чтение и действие.** Компонент, который суммирует произвольный web,
   не должен одновременно владеть tools для deploy или отправки сообщений.
2. **Передавайте дальше структурированные факты.** Преобразуйте текст в typed record с
   provenance. Не переносите скрытый HTML, инструкции и лишний raw content.
3. **Авторизуйте каждое действие на сервере.** Убедительный argument не даёт модели
   новых прав.
4. **Подтверждайте последствия.** Показывайте destination, mutation и источник
   запроса.
5. **Ограничивайте egress.** Tool для одного API не нужен произвольный интернет.
6. **Проверяйте rendered output.** Удаляйте опасный HTML и не загружайте автоматически
   model-generated URLs с приватными данными.

[OWASP Prompt Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html)
рассматривает least privilege, структурное разделение, output validation и human
approval как defense in depth. Универсального детектора среди них нет. Проектируйте так,
будто одна injection однажды пройдёт, и ограничивайте её последствия.

## Делайте узкие tools, а не remote shell

Лучшая validation начинается с маленького интерфейса. Сравните:

```text
execute_sql(query: string)
```

и:

```text
get_invoice(invoice_id: InvoiceId)
mark_invoice_reviewed(invoice_id: InvoiceId, reason: ReviewReason)
```

Во втором варианте сервер может проверить ownership, допустимые state transitions и
ограниченные values. JSON Schema ловит ошибки формы, но не знает, находится ли path
внутри разрешённого root и принадлежит ли object caller.

Для каждого tool проверяйте:

- types, lengths, formats, enums и request size;
- tenant и ownership по verified identity, а не по словам модели;
- paths после canonicalization и только внутри allowed root;
- URLs после parsing и DNS resolution, с egress policy;
- query parameters через prepared statements или фиксированный query builder;
- business transitions и idempotency keys;
- result size, content type и fields, возвращаемые модели.

Разделяйте read и write tools. Ставьте rate limit по principal и дорогой операции. У
каждого upstream call должны быть deadline и cancellation. Эти же решения упрощают
тестирование и эксплуатацию.

## Local server нужна process security, а не имитация OAuth

Актуальная MCP authorization spec рекомендует stdio implementations получать
credentials из environment, а не использовать HTTP authorization flow. Безопасным
локальный server от этого не становится: он работает с теми правами, которые дал host.

Перед установкой:

- проверьте точную command, arguments, источник package и pinned version;
- не запускайте mutable package install при каждом старте;
- выдайте только нужные directories, по возможности read-only;
- закройте network access, кроме необходимых named destinations;
- передайте только те secrets, которыми пользуется этот server;
- risky parsers и code execution запускайте в sandbox или container;
- оставьте stdio, если другим локальным процессам не нужен HTTP-доступ.

Официальный раздел про
[local MCP servers](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#local-mcp-server-compromise)
говорит прямо: server может исполняться с правами клиента. Удобная кнопка установки —
это consent на code execution, а не безобидная настройка.

## State handle — имя, а не credential

Длинный workflow часто возвращает `workflow_id`, `basket_id` или другой handle для
следующего call. На этом уровне протокол июля 2026 года stateless. Одного знания handle
не должно хватать для доступа к состоянию.

Генерируйте opaque и неугадываемые handles, ограничивайте lifetime и привязывайте на
сервере к verified user и tenant. Проверяйте binding на каждом call. Не доверяйте
`user_id`, который модель передала рядом с handle.

## Audit без новой утечки данных

Полезный audit event отвечает: кто попытался сделать что, с каким объектом, по какой
policy и с каким результатом:

```json
{
  "event": "mcp.tool_call",
  "request_id": "req_01...",
  "principal_id": "usr_internal_42",
  "server": "billing-tools",
  "tool": "mark_invoice_reviewed",
  "target_id": "inv_7f...",
  "decision": "allowed",
  "approval_id": "apr_01...",
  "scope_set": ["invoices:review"],
  "result": "success",
  "duration_ms": 184
}
```

Не логируйте bearer tokens, authorization codes, cookies, secret arguments, полные
email bodies и raw model context по умолчанию. Используйте internal IDs и field
allowlists. Отделите audit storage от записываемого сервиса, ограничьте доступ, задайте
retention и проверьте deletion, если этого требует policy.

## Security tests, которые должны быть в CI

Happy path через Inspector — не security test. Добавьте случаи:

- missing, expired, wrong-issuer и wrong-audience tokens;
- valid token с недостаточным scope;
- cross-tenant object IDs и украденные state handles;
- лишние JSON properties, oversized input, path traversal и injection strings;
- URLs, ведущие на loopback, private, link-local или redirected internal targets;
- tool output с indirect instructions и exfiltration links;
- изменение arguments после approval preview;
- duplicate writes, retries, timeouts и cancellation;
- seeded fake secrets в logs и traces;
- отказ authorization, policy, audit и upstream services.

После этого запускайте end-to-end agent scenarios. Полный
[гайд по тестированию MCP-сервера](/ru/blog/mcp-server-testing/) разбирает contract
snapshots, protocol-version matrix, transport failure injection и conformance. Эти
тесты доказывают, что сервер отклоняет плохой запрос, но не гарантируют безопасную
последовательность tools. [Гайд по evaluation AI-агентов](/ru/blog/ai-agent-testing-evaluation/)
разбирает trajectory и task-level regression gates.

## Checklist перед production

- [ ] У каждой trust boundary и категории данных есть owner.
- [ ] Remote token проверяется по issuer, audience, expiry и scope.
- [ ] Downstream API использует отдельный credential; token passthrough невозможен.
- [ ] Tool access выводится из verified identity и policy.
- [ ] Sensitive call требует argument-bound user approval.
- [ ] Tools задают узкие операции и отклоняют неизвестные fields.
- [ ] External content и tool results считаются untrusted data.
- [ ] Local process имеет явные filesystem, network и secret grants.
- [ ] Handles непрозрачны, истекают и привязаны к caller.
- [ ] Настроены rate limits, deadlines, output limits и cancellation.
- [ ] Audit events не содержат secrets и лишние personal data.
- [ ] Проверены revocation и emergency disable path.

Ни один пункт не делает агента «защищённым от prompt injection». Вместе они превращают
сбой модели из неограниченного инцидента в отклонённый запрос, ограниченное действие
или approval, на котором пользователь может остановиться.
