У вашего сервиса есть API-ключ к облачному хранилищу. Пользователь отправляет запрос. Ваш сервис добросовестно записывает файл. Файл попадает в бакет злоумышленника, а не ваш, и теперь у них есть ваши данные. Вы только что стали невольным пособником.

Это атака Confused Deputy. Злоумышленник не может самостоятельно записать что-либо в целевой бакет. У него нет разрешения. Но он может обмануть ваш сервис, у которого разрешение есть, и заставить его сделать это за него. Ваш сервис — “deputy”. Он “confused” в том, для кого на самом деле предназначено действие.

Как на самом деле выглядит атака Confused Deputy

Классический пример — компилятор. Компилятор работает с привилегиями вашего пользователя, поэтому может читать его исходные файлы и записывать объектные файлы в его домашний каталог. Злоумышленный пользователь подает компилятору исходный файл, содержащий директивы записать вывод в /etc/passwd вместо каталога сборки. У компилятора есть полномочия. У пользователя — нет. Компилятор не понимает, чьё намерение он выполняет.

Современные веб-сервисы постоянно воспроизводят этот паттерн. Рассмотрим сервис загрузки файлов, который позволяет пользователям указывать callback URL:

# The user provides a URL. We fetch it and store it.
def upload_from_url(user_id: str, source_url: str, dest_path: str):
    response = requests.get(source_url, timeout=30)
    s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)
    audit_log.info(f"User {user_id} uploaded to {dest_path}")

Это выглядит безобидно. Пользователь предоставляет URL. Вы загружаете его. Вы сохраняете его в свой бакет. Но что, если source_urlhttp://169.254.169.254/latest/meta-data/iam/security-credentials/? Теперь вы эксфильтрируете собственные AWS-учетные данные и записываете их по пути, который назвал злоумышленник. У вашей IAM-роли есть полномочия. У злоумышленника — нет. Вы — confused deputy.

Атака обобщается на любую систему, где привилегированный компонент выполняет действие от имени менее привилегированного запрашивающего, а запрашивающий может влиять на параметры этого действия.

Почему проверки идентичности недостаточны

Инстинктивное исправление — проверить, кто является запрашивающим. “Аутентифицирован ли этот пользователь?” Да. “Авторизован ли этот пользователь на загрузку файлов?” Да. Обе проверки проходят, и атака всё равно работает.

Проблема в том, что проверки авторизации подтверждают, может ли запрашивающий выполнить действие. Они не проверяют, подходит ли цель действия для этого запрашивающего. Пользователю разрешено загружать файлы. Но ему не разрешено заставлять ваш сервис загружать собственные метаданные и сохранять их под его контролем. Модель разрешений отождествляет “может использовать endpoint загрузки” с “может заставить сервис выполнять произвольные привилегированные действия”.

Это основной режим отказа: deputy владеет capability (AWS-учетные данные, доступ к файловой системе, доступ на запись в базу данных) и использует её на основании инструкций от кого-то другого. Без привязки capability к конкретному намерению исходного владельца полномочий путаница неизбежна.

Capability-Based Security: привязка полномочий к намерению

Capability-based security исправляет это, делая полномочия неподделываемыми и явными. Вместо того чтобы спрашивать “кто вы?” и затем искать, что вам разрешено делать, capability — это токен, который напрямую передает право выполнять конкретное действие над конкретным ресурсом.

В системе capability сервис загрузки не хранил бы универсальный ключ на запись в S3. Он хранил бы capability на запись в конкретные пути, ограниченные конкретными пользователями. Запрос пользователя включал бы capability token, указывающий точное назначение. Сервис проверял бы криптографическую подпись токена и его область действия, а не идентичность пользователя по глобальной таблице разрешений.

Вот упрощенный пример с использованием scoped tokens:

import hashlib
import hmac
import secrets

SECRET = secrets.token_bytes(32)

def mint_capability(principal: str, action: str, resource: str) -> str:
    """Create an unforgeable capability token scoped to a specific action and resource."""
    payload = f"{principal}:{action}:{resource}"
    sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
    return f"{payload}:{sig}"

def verify_capability(token: str, expected_action: str, expected_resource: str) -> bool:
    """Verify a capability token matches the expected action and resource."""
    try:
        principal, action, resource, sig = token.rsplit(":", 3)
    except ValueError:
        return False
    payload = f"{principal}:{action}:{resource}"
    expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
    return hmac.compare_digest(sig, expected_sig) and action == expected_action and resource == expected_resource

# The user receives a capability that only allows writing to their own prefix.
user_id = "alice"
scoped_token = mint_capability(user_id, "write", f"uploads/{user_id}/report.pdf")

# Later, the service verifies the capability before acting.
if not verify_capability(scoped_token, "write", "uploads/alice/report.pdf"):
    raise PermissionError("Capability does not match requested action or resource")

s3.put_object(Bucket="my-app-bucket", Key="uploads/alice/report.pdf", Body=data)

Ключевое понимание: capability token scoped_token нельзя replay, чтобы записать в uploads/bob/salary.xlsx. Он привязан к конкретному ресурсу на момент создания. Сервис не решает, что разрешено, на основе того, кто спрашивает. Он решает на основе того, что токен явно авторизует.

Когда capability-системы непрактичны

Большинство из нас не строит операционные системы с чистыми capability-архитектурами. Мы вызываем AWS API, работаем с PostgreSQL и пишем HTTP-обработчики. Полноценная capability-based security — это философия проектирования, а не библиотека, которую можно подключить.

Практичный компромисс — применить capability-подход к границам вашего сервиса. Вместо того чтобы давать вашему сервису одну IAM-роль с широкими S3-разрешениями, используйте scoped presigned URLs. Вместо того чтобы позволять пользователям называть произвольные назначения, валидируйте и санитизируйте целевой ресурс относительно того, к чему этот пользователь имеет право прикасаться. Цель — не формальная capability-ОС. Цель — устранение неявных полномочий.

Вот тот же endpoint загрузки с scoped validation вместо безоговорочного доверия:

import re

# A regex that constrains what a user can name as a destination.
ALLOWED_DEST_PATTERN = re.compile(r"^uploads/(?P<user_id>[a-z0-9_-]+)/[a-zA-Z0-9_.-]+$")

def upload_from_url(user_id: str, source_url: str, dest_path: str):
    match = ALLOWED_DEST_PATTERN.match(dest_path)
    if not match or match.group("user_id") != user_id:
        raise ValueError("Invalid destination path")

    # The source URL is also restricted. No internal IPs, no metadata endpoints.
    parsed = urlparse(source_url)
    if parsed.hostname in ("169.254.169.254", "localhost", "127.0.0.1"):
        raise ValueError("Forbidden source URL")

    response = requests.get(source_url, timeout=30)
    s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)

Это не capability-система в формальном смысле. Но она применяет тот же принцип: полномочия сервиса — не чистый лист. Они ограничены конкретным набором действий, которые запрашивающий имеет право вызывать.

Почему этот паттерн постоянно возвращается

Уязвимости Confused Deputy проявляются в OAuth flows, serverless-функциях, webhook-обработчиках и supply-chain pipeline. Каждый раз, когда компонент с повышенными привилегиями обрабатывает пользовательский ввод и действует на его основе, риск присутствует.

Паттерн SSRF-to-metadata-exfiltration настолько распространен, что облачные провайдеры теперь по умолчанию блокируют IMDS IP в VPC. Это смягчение одного симптома, а не болезни. Болезнь — привилегированный компонент, который не может отличить свое намерение от намерения запрашивающего.

Предотвращение атак Confused Deputy на практике

Вам не нужно переписывать архитектуру. Три привычки покрывают большинство случаев.

Ограничивайте область действия ваших учетных данных. Если ваш сервис работает с S3, дайте ему IAM-политику, которая может записывать только в конкретный префикс. Если он работает с базой данных, используйте row-level security или отдельные учетные данные для каждого tenant. Чем меньше blast radius confused deputy, тем меньше ущерба он может нанести.

Валидируйте цель, а не только актора. Проверки авторизации должны отвечать на два вопроса: разрешено ли запрашивающему выполнить это действие, и находится ли цель действия в области действия запрашивающего? Пользователь может загружать файлы. Может ли он загружать файлы в каталог другого пользователя? Может ли он заставить ваш сервис вызывать произвольные URL? Второй вопрос — там, где обитают confused deputy.

Избегайте ambient authority. Ambient authority — это когда процесс имеет разрешения просто потому, что он такой, а не потому, что он делает. Работа от root. Использование master API-ключа. Удержание соединения с database superuser. Это удобно и опасно. Разделяйте свои привилегии. Используйте временные учетные данные с явным сроком действия. Чем более детальны ваши полномочия, тем сложнее их запутать.

FAQ

Является ли CSRF атакой Confused Deputy?

Да, в некотором смысле. Браузер пользователя — deputy. Он хранит session cookie (capability) и выполняет действие, изменяющее состояние, потому что вредоносный сайт сказал ему это сделать. Браузер не понимает, представляет ли запрос намерение пользователя. CSRF-токены работают, привязывая capability (cookie) к конкретному контексту действия, что и есть capability-based исправление.

Применимо ли это к микросервисам?

Абсолютно. Вызовы между сервисами часто используют общий service account с широкими разрешениями. Если Сервис A вызывает Сервис B с параметром, предоставленным пользователем, и Сервис B использует свой привилегированный аккаунт, чтобы действовать на основе этого параметра, Сервис B — confused deputy. Используйте scoped service tokens или request-specific authorization.

А что насчет SQL injection?

SQL injection — это атака Confused Deputy на вашу базу данных. База данных — deputy. У неё есть полномочия на чтение и запись таблиц. Злоумышленник подает ей инструкции, замаскированные под ввод. Parameterized queries исправляют это, разделяя полномочия базы данных (структуру запроса) и пользовательский ввод (параметры).

Аудитируйте ваших deputy

Пройдитесь по вашим сервисам и определите те, которые обладают привилегиями, которых нет у пользователей. Для каждого спросите: может ли пользователь обмануть этот сервис, заставив его использовать свои полномочия так, что это принесет пользу злоумышленнику? Если ответ да, у вас есть confused deputy. Исправление редко бывает экзотическим. Обычно это вопрос ограничения того, что сервис готов делать, и отказа в запросах, выходящих за эти рамки.

Ваш сервис не должен быть пособником. Убедитесь, что он знает, чьи приказы он выполняет.