Как работать с токенами олицетворения Windows — олицетворение на уровне потока и безопасный откат

· Обновлено: · · Windows, Безопасность, Токен доступа, Олицетворение, Win32, .NET, C#, Сопровождение, Повторное использование существующих систем

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619890)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Как работать с токенами олицетворения Windows — олицетворение на уровне потока и безопасный откат. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/06/09/002-windows-impersonation-token/

DOI (зарегистрированный архив)
10.5281/zenodo.21619890
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619891

1. Что нужно понять в первую очередь

При разработке Windows-приложений и Windows-служб иногда возникает задача: «выполнить именно эту операцию от имени другого пользователя».

Например:

  • из Windows-службы обратиться к файловому серверу с правами самого пользователя;
  • в административном приложении проверить только то, что видно конкретному пользователю;
  • в Named Pipe, RPC, COM, IIS, ASP.NET Core и похожих технологиях выполнить часть обработки с правами вызывающего пользователя;
  • из-за уже имеющейся инфраструктуры переключать учётную запись Windows отдельно для разных операций.

Именно здесь появляются олицетворение, токен доступа и токен олицетворения.

Но сразу стоит подчеркнуть одно.

Олицетворение в Windows — это не «магия, превращающая вас в администратора». Это механизм, который переключает контекст безопасности, используемый при проверке доступа, в основном на уровне отдельного потока.

Если реализовать это, не поняв данное отличие, возникают такие проблемы:

  • вы думаете, что олицетворяете пользователя, а доступ к файлу всё равно заканчивается Access denied;
  • локальные файлы читаются, а сетевые общие папки — нет;
  • где-то посреди Task.Run или async вы незаметно возвращаетесь к исходному пользователю;
  • олицетворение остаётся активным вплоть до записи логов и последующей обработки, и граница прав размывается;
  • первичный токен и токен олицетворения перепутаны, и запуск процесса заканчивается ошибкой;
  • возникает ступор: «пользователь состоит в группе Administrators, почему тогда нельзя записать файл?»

В этой статье собраны практические правила, как безопасно работать с токенами олицетворения Windows.

Речь не о техниках атак и не о захвате привилегий. Это статья о том, как корректно держать границу прав в Windows-приложениях, Windows-службах и приложениях на .NET.

Весь код из статьи опубликован на GitHub как собираемый набор примеров: библиотека, демонстрационное приложение для Windows и модульные тесты, которые проверяют аргументы и защитное поведение.

windows-impersonation-token - komurasoft-blog-samples (GitHub)

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 18, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Что такое токен доступа

В Windows контекст безопасности пользователя или процесса представляют через токен доступа.

В токене доступа хранится примерно такая информация. Эти термины ещё не раз появятся в следующих главах, поэтому к каждому добавлено короткое пояснение.

Поле Смысл
SID пользователя SID — Security Identifier, идентификатор, однозначно обозначающий пользователя или группу. Проверка доступа идёт по этому значению, а не по отображаемому имени
Группы Набор SID групп, в которые входит пользователь. Членство в Administrators тоже видно здесь
Права, Privilege Привилегии вроде «операции резервного копирования» или «завершение работы», которые выдаются отдельно от ACL конкретного объекта. У каждой есть состояние «включена / выключена»
Владелец по умолчанию SID, который становится владельцем объектов, созданных с этим токеном
DACL по умолчанию DACL — Discretionary Access Control List, список того, кому что разрешено. Его по умолчанию получают вновь созданные объекты
Ограничивающие SID Список SID, который используется в ограниченном токене. По этому списку выполняется дополнительная проверка доступа, поэтому членства в группе может оказаться недостаточно
Уровень целостности Иерархия вроде Low, Medium, High. С более низкого уровня целостности нельзя записывать в объекты более высокого. Об этом — в главе 17
Состояние повышения В среде с включённым UAC показывает, повышен ли уже этот токен. Об этом — в главе 17
Уровень олицетворения Значение есть только у токена олицетворения. От него зависит, можно ли только идентифицировать клиента, реально обращаться к объектам или делегировать права удалённо. Об этом — в главе 7
Тип токена Различие между первичным токеном и токеном олицетворения. Об этом — в главе 6

Здесь важно запомнить: на проверку доступа влияют не имя, а SID, группы и уровень целостности. Тогда в следующих главах проще понять ситуацию «имя верное, а всё равно Access denied».

Многие объекты Windows — файлы, ключи реестра, службы, именованные каналы, процессы, потоки, события, мьютексы и другие — имеют дескриптор безопасности.

Когда поток пытается открыть защищённый объект, Windows сверяет данные токена со списком ACL этого объекта.

Поток, выполняющий операцию
  ↓
В каком контексте безопасности выполняется доступ
  ↓
Смотрим пользователя, группы и права токена
  ↓
Сверяем их с ACL целевого объекта
  ↓
Решение: разрешить / отказать

Понять, «в каком контексте безопасности выполняется доступ», — это и есть вход в тему токенов олицетворения.

3. У процесса есть первичный токен

У каждого процесса Windows обычно есть первичный токен доступа.

Например, когда пользователь запускает приложение с рабочего стола, процессу назначается первичный токен, который представляет контекст безопасности этого пользователя.

Для Windows-службы это будет первичный токен учётной записи, от имени которой служба выполняется.

Схематично это выглядит так:

MyService.exe
  Primary Token: DOMAIN\svc-app

Если поток внутри этой службы никого не олицетворяет, то при обращении к файлам и реестру используется первичный токен процесса.

То есть по умолчанию картина такая:

Thread A
  Impersonation Token: нет
  ↓
При проверке доступа используется Primary Token процесса

В таком состоянии открытие C:\Data\foo.txt проверяет, есть ли права доступа у DOMAIN\svc-app.

4. Токен олицетворения присоединяется к потоку

Когда начинается олицетворение, к потоку присоединяется токен олицетворения. Это ключевой момент: олицетворение — это в первую очередь не «весь процесс становится другим пользователем», а точнее проверка доступа для этого конкретного потока в другом контексте безопасности.

MyService.exe
  Primary Token: DOMAIN\svc-app

Thread A
  Impersonation Token: DOMAIN\alice

Thread B
  Impersonation Token: нет

Если в этот момент Thread A открывает файл, проверка доступа идёт с правами DOMAIN\alice.

Thread B никого не олицетворяет, поэтому проверка доступа для него идёт с правами DOMAIN\svc-app.

Без понимания этого различия возникает вот такая путаница:

// Думали, что олицетворяем в Thread A
StartImpersonation(token);

// Но работа уходит в другой поток
Task.Run(() =>
{
    File.ReadAllText(path);
});

// И сразу же откатываем
RevertToSelf();

В этом случае поток, который фактически читает файл, не обязательно работает в ожидаемом состоянии олицетворения.

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

5. «Олицетворение» — это не повышение прав

Слово «олицетворение» звучит довольно сильно, но на практике важно не путать его с «повышением прав».

По сути олицетворение позволяет вот что:

Обрабатывать запрос с правами серверного процесса
  ↓
Для части операций выполнять проверку доступа с правами клиентского пользователя

Например, если ACL файлового сервера нужно использовать как есть, как механизм контроля доступа, а серверное приложение всегда читает файлы от служебной учётной записи, ACL конкретного пользователя не применятся.

Поэтому для части обработки запроса выполняют олицетворение вызывающего пользователя и уже под ним обращаются к файлу.

Запрос HTTP / RPC / Named Pipe
  User: DOMAIN\alice
      ↓
Серверное приложение
  Process: DOMAIN\svc-app
      ↓
Только для доступа к файлу олицетворяем DOMAIN\alice
      ↓
ACL файлового сервера разрешает / запрещает доступ

Это полезно, когда нужны уже существующие ACL Windows, а не собственная логика авторизации приложения.

Но при всём удобстве олицетворения ошибки в проектировании легко размывают границу прав.

  • какая операция выполняется от чьего имени;
  • где олицетворение начинается;
  • где оно гарантированно снимается;
  • с правами какого пользователя пишется тот или иной лог;
  • снимается ли олицетворение и при исключении;
  • остаётся ли оно активным до конца асинхронной обработки.

Всё это нужно сделать явным прямо в коде.

6. Разделяйте первичный токен и токен олицетворения

Среди токенов Windows чаще всего путают первичный токен и токен олицетворения.

В общих чертах их можно представить так:

Токен Основное назначение Типичные примеры
Первичный токен Представляет контекст безопасности процесса Запуск процесса, CreateProcessAsUser
Токен олицетворения Нужен, чтобы поток работал в другом контексте безопасности ImpersonateLoggedOnUser, SetThreadToken, олицетворение клиента в Named Pipe

Особенно важно помнить: чтобы запустить процесс, как правило нужен именно первичный токен.

Наличие токена олицетворения ещё не значит, что его можно напрямую использовать для запуска процесса от другого пользователя.

Типичная последовательность выглядит так:

Олицетворяем клиента
  ↓
Получаем токен олицетворения через OpenThreadToken
  ↓
Создаём первичный токен через DuplicateTokenEx
  ↓
Передаём его, например, в CreateProcessAsUser

И наоборот: если задача — «в этом потоке только обратиться к файлу от другого пользователя», речь идёт не о запуске процесса, а именно о токене олицетворения.

Если смешать эти два случая, вы будете озадачены ошибками вроде Access denied или The parameter is incorrect, хотя аргументы API вроде бы верны.

7. Разбираемся в уровнях олицетворения

У токена олицетворения есть уровень олицетворения.

Есть четыре основных уровня.

Уровень олицетворения Примерный смысл
Anonymous Сервер не может получить сведения о личности клиента
Identification Сервер может определить, кто такой клиент, но не может использовать его права для доступа к объектам
Impersonation Сервер может действовать с правами клиента в пределах локальной системы
Delegation Сервер может делегировать права клиента и удалённым системам

На практике чаще всего спотыкаются на разнице между Identification и Impersonation.

Identification, как следует из названия, — уровень, чтобы узнать, кто перед вами. Чтобы открывать файлы с правами этого пользователя, его недостаточно.

Поэтому случается вот что:

WindowsIdentity.GetCurrent().Name показывает ожидаемое имя пользователя
  ↓
Но доступ к файлу всё равно заканчивается Access denied

В этом случае нужно проверять не только имя, но и уровень олицетворения.

В .NET подсказку даёт свойство WindowsIdentity.ImpersonationLevel.

using System.Security.Principal;

WindowsIdentity identity = WindowsIdentity.GetCurrent();

Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);

При доступе через сеть нужна ещё большая осторожность.

В схемах вроде «олицетворить пользователя на веб-сервере и от его имени обратиться к другому файловому серверу или серверу БД» можно упереться в так называемую проблему двойного прыжка (double hop).

Простое олицетворение в коде приложения эту проблему не обязательно решает. Нужно проектировать решение целиком: Kerberos, SPN, делегирование, ограниченное делегирование, служебные учётные записи и способ аутентификации целевой системы.

8. Базовая схема олицетворения

Концептуально работа с олицетворением через Win32 API выглядит так:

1. Получить токен, который будет использоваться для олицетворения
2. Олицетворить текущий поток этим токеном
3. Выполнить только необходимую операцию
4. Обязательно вернуться в исходный контекст безопасности
5. Закрыть хендл токена

В коде это всегда оформляется через try / finally.

if (!ImpersonateLoggedOnUser(tokenHandle))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    // Только этот фрагмент выполняется от имени олицетворённого пользователя
    DoWorkAsImpersonatedUser();
}
finally
{
    if (!RevertToSelf())
    {
        // Если откат не удался, это опасно: как минимум нельзя продолжать обработку
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

По времени это выглядит так:

файлWindowsрабочий потоквызывающая сторонафайлWindowsрабочий потоквызывающая сторонас этого момента проверки доступаидут от имени пользователя aliceдалее снова служебная учётная записьsvc-appзапрашивает операциюImpersonateLoggedOnUser назначает токеноткрывает файлACL проверяется с правами aliceRevertToSelf снимает токенвозвращает результат

Олицетворение действует только на участке от ImpersonateLoggedOnUser до RevertToSelf. Ввод-вывод за пределами этого участка оценивается уже не от олицетворённого пользователя, а от учётной записи процесса. Ловушки асинхронной обработки из главы 14 как раз про то, что фактический ввод-вывод вылезает за этот участок.

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

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

Поэтому олицетворение стоит воспринимать не как «начал — потом откатил», а как заключение в как можно более узкую область.

9. В .NET используйте WindowsIdentity.RunImpersonated

В .NET, где это возможно, стоит использовать WindowsIdentity.RunImpersonated — так область олицетворения проще выразить прямо в коде.

Если есть SafeAccessTokenHandle, можно написать так:

using Microsoft.Win32.SafeHandles;
using System.Security.Principal;

static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
    return WindowsIdentity.RunImpersonated(token, () =>
    {
        return File.ReadAllText(path);
    });
}

Преимущество такого подхода в том, что область олицетворения замкнута внутри лямбда-выражения.

RunImpersonated(token, () =>
{
    // Олицетворение действует только здесь
});

// За пределами этого блока — исходный контекст

Если нужно олицетворить только конкретную операцию — доступ к файлу, к реестру, вызов существующей библиотеки, — такая форма читается легко и безопасна.

Для асинхронной работы используется RunImpersonatedAsync.

using Microsoft.Win32.SafeHandles;
using System.Security.Principal;

static Task WriteFileAsUserAsync(
    SafeAccessTokenHandle token,
    string path,
    string text,
    CancellationToken cancellationToken)
{
    return WindowsIdentity.RunImpersonatedAsync(token, async () =>
    {
        await File.WriteAllTextAsync(path, text, cancellationToken);
    });
}

Чего стоит избегать — это запускать fire-and-forget задачи изнутри области олицетворения.

// Плохой пример
WindowsIdentity.RunImpersonated(token, () =>
{
    _ = Task.Run(() =>
    {
        File.WriteAllText(path, text);
    });
});

В этом коде непонятно, когда именно и в каком контексте выполнения на самом деле выполнится запись в файл.

Асинхронную работу, которая должна идти под олицетворением, нужно ожидать (await) внутри RunImpersonatedAsync и выходить из области только после её завершения.

10. Получение токена через LogonUser

Типичный API, чтобы получить токен по учётным данным другого пользователя, — LogonUser, но пользоваться им нужно осторожно.

В LogonUser передаются имя пользователя, домен и пароль. То есть само приложение начинает работать с учётными данными.

На практике здесь стоит обратить внимание на следующее:

  • не хранить пароль в открытом виде в коде или файлах конфигурации;
  • по возможности использовать аутентификацию ОС, служебные учётные записи, делегирование или уже имеющуюся аутентификацию Windows;
  • хранить секреты в подходящем хранилище секретов или средствами эксплуатационной инфраструктуры;
  • обязательно закрывать хендлы токенов;
  • не выводить в логи ничего секретного, кроме имени пользователя;
  • сужать область олицетворения.

Ниже будет минимальный пример, но не копируйте его как есть и не подставляйте строковый литерал в password. Учётные данные, записанные в исходный код, остаются в истории репозитория, в артефактах сборки и в результате декомпиляции. Хранение пароля открытым текстом в файле конфигурации — то же самое. К этому ещё вернёмся в разделе 21.6.

Куда тогда класть пароль? Варианты такие.

Способ хранения Когда уместен На что смотреть
Вообще не держать пароль Первый кандидат. Закрыть задачу аутентификацией Windows, служебной учётной записью или делегированием Нужно решить на этапе проектирования. Убрать пароль потом уже сложно
Запросить интерактивно в момент запуска Административный инструмент, который человек запускает у себя Не подходит для службы и для запуска без оператора. Прочитанную строку нужно сразу отпускать после использования
Диспетчер учётных данных Windows Повторное использование на том же ПК и под той же учётной записью Это хранилище на пользователя. Если работает служба, регистрировать нужно под её учётной записью
DPAPI Защитить значение в файле конфигурации внутри одного ПК Смотрите область ProtectedData. При CurrentUser расшифрует только тот пользователь, который зашифровал; при LocalMachine расшифровать смогут и другие пользователи этого ПК
Платформа управления секретами вроде Key Vault Несколько серверов, CI, связка с облаком Нужна аутентификация самой платформы. Как обращаться с секретом уже в памяти — отдельный вопрос

Конкретный способ работы с DPAPI разобран в отдельной статье «Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI».

Общее для всех вариантов одно: прежде чем выбирать способ хранения, спросите, нужно ли приложению вообще принимать пароль.

Минимальный пример:

using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;

internal static class NativeMethods
{
    private const int LOGON32_LOGON_INTERACTIVE = 2;
    private const int LOGON32_PROVIDER_DEFAULT = 0;

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern bool LogonUser(
        string lpszUsername,
        string? lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        out SafeAccessTokenHandle phToken);

    public static SafeAccessTokenHandle Logon(
        string userName,
        string? domain,
        string password)
    {
        bool ok = LogonUser(
            userName,
            domain,
            password,
            LOGON32_LOGON_INTERACTIVE,
            LOGON32_PROVIDER_DEFAULT,
            out SafeAccessTokenHandle token);

        if (!ok)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        return token;
    }
}

public static string ReadFileWithExplicitCredential(
    string userName,
    string? domain,
    string password,
    string path)
{
    using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);

    return WindowsIdentity.RunImpersonated(token, () =>
    {
        return File.ReadAllText(path);
    });
}

Этот пример нужен лишь для того, чтобы показать форму API.

На практике сначала стоит задать вопрос: а нужно ли вообще приложению принимать пароль на вход?

Во многих случаях безопаснее рассмотреть такие альтернативы:

Что нужно сделать Альтернатива
Всей службе нужен доступ к определённому ресурсу Выделить специальную служебную учётную запись с минимально необходимыми ACL
Нужен доступ к файлу с правами самого пользователя Использовать аутентификацию Windows и заранее спроектировать делегирование
Нужна операция, требующая прав администратора Создать на стороне службы явный административный API и контролировать его авторизацией приложения
Только часть обработки должна идти от другой учётной записи Заключить олицетворение в границы конкретного метода и вести журнал аудита

11. Смотрите на тип входа (logon type) в LogonUser

Свойства токена, который возвращает LogonUser, зависят от типа входа (logon type).

Если скопировать код, не разобравшись в этом различии, он не будет работать так, как ожидается.

Тип входа На что обратить внимание
Interactive Близко к интерактивному входу. Удобен для локальных операций, но зависит от окружения выполнения и прав
Network Предназначен для сетевого входа. Возвращённый токен не всегда можно напрямую использовать для запуска процесса
NewCredentials Локально ведёт себя почти как текущие учётные данные, а указанные учётные данные применяются при удалённых подключениях

Речь не о том, чтобы заучить конкретные типы входа наизусть.

Важны два момента.

  1. Тип входа меняет поведение при локальном доступе, сетевом доступе и запуске процессов.
  2. Нужно проверять, каким получен возвращённый токен — первичным или токеном олицетворения.

Типичная путаница — передать токен, полученный с LOGON32_LOGON_NETWORK, напрямую в CreateProcessAsUser и получить ошибку.

Если цель — запуск процесса, нужен первичный токен. При необходимости его создают из имеющегося токена через DuplicateTokenEx.

12. Не относитесь к RevertToSelf легкомысленно

Если олицетворение запущено через Win32 API, завершается оно вызовом RevertToSelf. Этот откат — не просто уборка за собой, а важная операция, которая возвращает границу безопасности на место.

Плохой пример:

ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();

На первый взгляд всё выглядит нормально, но если в DoWork() возникнет исключение, RevertToSelf() не будет вызван.

Поэтому обязательно используйте finally:

if (!ImpersonateLoggedOnUser(token))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    DoWork();
}
finally
{
    if (!RevertToSelf())
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

Продолжать работу после неудачного RevertToSelf тоже опасно. Если исходные права, возможно, не восстановлены, дальнейшая обработка пойдёт с правами не того пользователя. Как минимум эту операцию нужно считать неуспешной и выбрать безопасный вариант.

RunImpersonated / RunImpersonatedAsync в .NET полезны именно как способ выразить область, который помогает не забыть про откат.

13. Делайте область олицетворения как можно меньше

Самый важный принцип проектирования при работе с олицетворением — олицетворять только то, что действительно нужно.

Плохой пример:

WindowsIdentity.RunImpersonated(token, () =>
{
    ValidateRequest();
    LoadConfiguration();
    WriteDebugLog();
    ReadUserFile();
    UpdateDatabase();
    SendNotification();
});

Если олицетворять настолько широкую область, становится непонятно, какая операция выполняется с какими правами.

Например, обращение к месту вывода логов может пойти с правами олицетворённого пользователя, и запись в лог завершится ошибкой. Подключение к БД может попытаться пройти не с учётными данными служебной учётной записи, а с данными олицетворённого пользователя. Даже отправка уведомлений и создание временных файлов может неожиданно попасть под ненужный контекст прав.

Хороший пример — вынести в отдельный блок только те операции, которым действительно нужно олицетворение.

ValidateRequest();
LoadConfiguration();

string content = WindowsIdentity.RunImpersonated(token, () =>
{
    return File.ReadAllText(userFilePath);
});

UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);

В таком виде сразу видно, что олицетворение нужно только для File.ReadAllText.

Олицетворение удобно, но чем шире его область, тем сложнее читать код и тем легче допустить ошибку.

14. В асинхронном коде проверяйте, «остаётся ли олицетворение активным до конца»

В современных приложениях на .NET многие операции — с файлами, HTTP, БД, очередями, хранилищами — выполняются асинхронно.

Поэтому сочетание олицетворения с async / await требует внимания.

Базовое правило только одно:

Асинхронную работу, которой нужно олицетворение, нужно await-ить внутри RunImpersonatedAsync

Что будет, если правило нарушить, удобно смотреть в той же форме, что и схема из главы 8. Но сначала важно другое. «Отправили в другой поток — олицетворение снялось» — это не всегда так. Как именно ломается код, зависит от того, каким механизмом вы олицетворяли, и ломается он в противоположные стороны.

Как олицетворяли Задача, ушедшая в Task.Run Что происходит
RunImpersonated / RunImpersonatedAsync в .NET продолжает работать олицетворённой Олицетворение длится дольше, чем область в коде. Вызвавшая сторона не получает ни завершения, ни исключения
Прямой вызов Win32 ImpersonateLoggedOnUser работает от учётной записи процесса Фактический ввод-вывод выходит за участок олицетворения и заканчивается Access denied

На стороне .NET так выходит потому, что RunImpersonated кладёт токен олицетворения в AsyncLocal. Значение AsyncLocal течёт вместе с ExecutionContext, а Task.Run захватывает ExecutionContext на момент вызова и восстанавливает его на потоке пула. Обработчик изменения, который срабатывает при каждом восстановлении, заново вызывает ImpersonateLoggedOnUser на этом потоке, поэтому тот же токен олицетворения появляется и на потоке пула. Так устроена реализация среды выполнения (s_currentImpersonatedToken и CurrentImpersonatedTokenChanged в WindowsIdentity).

Токен Win32, напротив, привязан к потоку и сам по себе на другой поток не копируется.

Если проследить сторону .NET по времени, получается так:

файлдругой поток пулавызывающий потокфайлдругой поток пулавызывающий потоквместе с ExecutionContextпередаётся и токен олицетворенияснимается олицетворениетолько этого потокапо-прежнему от имени олицетворённого пользователя.вызывающая сторона не знает, когда это закончитсяRunImpersonated начинает олицетворениеTask.Run отправляет записьвыходит из области и снимает олицетворениефактическая запись выполняется позжерезультат и исключение никто не получает

На схеме из главы 8 ввод-вывод оставался внутри участка олицетворения. Здесь в момент Task.Run работа уходит в другой поток, и фактическая запись оказывается за пределами той области, которую видно в коде. Опасно не то, что олицетворение снимается, а то, что оно не снимается и его уже нельзя отследить. Конкретно одновременно происходят три вещи.

  • Границы олицетворения больше не читаются. Область, которую вы записали как «отсюда и досюда», не совпадает с областью, которая реально работает под олицетворением.
  • Сбой проглатывается. Задачу никто не await-ит, поэтому даже при Access denied исключение нигде не всплывает.
  • Непонятно, когда закрывать токен. Если закрыть SafeAccessTokenHandle через using, вы закроете его под ногами у ещё работающей операции.

Если олицетворять напрямую через Win32, картина обратная: отправленная работа идёт от учётной записи процесса. В тестовой среде у служебной учётной записи часто тоже есть права, поэтому ошибка всплывает уже на продакшене как Access denied.

В обоих случаях этого не будет, если дождаться завершения внутри RunImpersonatedAsync.

Хороший пример:

await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
    await using FileStream stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    string text = await reader.ReadToEndAsync();

    await ProcessTextAsync(text);
});

Но и в такой форме есть о чём подумать.

Нужно ли, чтобы ProcessTextAsync тоже выполнялся от имени олицетворённого пользователя?

Если олицетворение нужно только для чтения файла, безопаснее разделить код так:

string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
    return await File.ReadAllTextAsync(path);
});

await ProcessTextAsync(text);

То, что внутри области олицетворения можно использовать await, ещё не значит, что туда стоит класть всё подряд.

И в асинхронном коде область олицетворения нужно сужать.

15. ASP.NET Core и олицетворение

При использовании аутентификации Windows в ASP.NET Core тоже нужно аккуратно работать с олицетворением.

Опасно считать, что раз пользователь вошёл через аутентификацию Windows, вся обработка запроса автоматически идёт от его имени.

Как правило, сам процесс приложения работает от имени идентификатора пула приложений или служебной учётной записи. Windows-идентификатор пользователя доступен как сведения об аутентификации, но это не значит, что вся дальнейшая обработка автоматически идёт с правами пользователя.

Если конкретное действие нужно выполнить с правами пользователя, явно создавайте область через RunImpersonated / RunImpersonatedAsync.

Пример кода:

app.MapGet("/download", async (HttpContext context) =>
{
    if (context.User.Identity is not WindowsIdentity user)
    {
        return Results.Unauthorized();
    }

    string path = GetPathFromRequest(context);

    byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
        user.AccessToken,
        async () => await File.ReadAllBytesAsync(path));

    return Results.File(bytes, "application/octet-stream");
});

И в этом примере олицетворяется только чтение файла.

Стоит ли включать в область олицетворения ещё и формирование ответа, логирование и проверку авторизации на стороне приложения — вопрос, который нужно обдумывать отдельно и осторожно.

16. Как разбираться с Access denied

Самая частая ошибка в реализациях с олицетворением — Access denied.

Если при её появлении думать только «олицетворение не удалось», поиск уходит в долгий и лишний обход.

Разобьём проверку по отдельным аспектам.

Аспект Что проверить
Действительно ли выполняется олицетворение Проверить WindowsIdentity.GetCurrent().Name внутри области олицетворения
Достаточен ли уровень олицетворения Убедиться, что это не Identification, а нужный уровень
Корректен ли ACL целевого ресурса Есть ли у олицетворённого пользователя права на чтение / запись
Локальный ресурс или удалённый Не получается ли так, что локальные файлы открываются, а UNC — нет
Не проблема ли это двойного прыжка Не пытаетесь ли вы пройти с веб-сервера на файловый сервер с правами пользователя
Не вышли ли вы за пределы области олицетворения Не выполняется ли фактический ввод-вывод вне области или в другой задаче
Верен ли тип токена Не передаётся ли токен олицетворения туда, где он нужен для запуска процесса
Нет ли влияния UAC / уровня целостности Не является ли токен не повышенным, даже если пользователь состоит в Administrators

Особенно важно не успокаиваться, проверив только имя пользователя.

WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);

Такой лог полезен, но его недостаточно.

Как минимум стоит также посмотреть на:

Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);

И если целевой ресурс — сетевая общая папка, проверяйте не только код приложения, но и способ аутентификации, настройки делегирования, SPN, служебную учётную запись и ACL на самом файловом сервере.

17. UAC и проблема «я администратор, но операция не проходит»

В Windows членство пользователя в группе Administrators и то, что текущий токен уже повышен, — не одно и то же.

В окружениях с включённым UAC даже у пользователя-администратора процессы обычно запускаются с ограниченным токеном, а для операций, требующих прав администратора, нужно явное повышение.

Поэтому случается вот что:

Пользователь состоит в группе Administrators
  ↓
Но текущий токен не повышен
  ↓
Запись в Program Files или HKLM заканчивается Access denied

С олицетворением всё то же самое.

Не стоит думать «олицетворяемый пользователь — администратор, значит запись пройдёт»: нужно проверять фактическое состояние переданного токена.

При отладке стоит смотреть на такие моменты:

  • группы, в которые входит пользователь;
  • включены или выключены отдельные Privilege;
  • уровень целостности;
  • состояние повышения прав;
  • является ли токен ограниченным;
  • есть ли связанный повышенный токен.

В Win32 API это можно проверить через GetTokenInformation, получая TokenType, TokenImpersonationLevel, TokenElevationType, TokenIntegrityLevel и другие атрибуты.

Тем не менее с точки зрения проектирования безопаснее не «олицетворять администратора и делать что угодно», а заключить необходимые минимальные операции в отдельную службу или административный API.

18. Сетевые общие папки и проблема двойного прыжка

Один из самых частых вопросов про олицетворение касается доступа к сетевым общим папкам.

Клиентский ПК
  ↓ аутентификация Windows
Веб-сервер / API-сервер
  ↓ хотим получить доступ через олицетворение
Файловый сервер

В такой конфигурации бывает так, что «на веб-сервере имя пользователя получить удаётся, а обращение к файловому серверу заканчивается ошибкой».

Это вопрос о том, можно ли повторно делегировать учётные данные пользователя другому серверу.

Олицетворение на локальном сервере и делегирование другому серверу — не одно и то же.

Уровня Impersonation может хватать для локальных операций, но его может быть недостаточно, чтобы выступать в роли клиента перед удалённым сервером.

Чтобы использовать права самого пользователя через сеть, нужно спроектировать делегирование Kerberos, ограниченное делегирование, SPN, служебные учётные записи и способ аутентификации.

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

Направление Что делать Когда уместно На что смотреть
Ограниченное делегирование Kerberos На учётной записи промежуточного сервера задать «делегировать можно только этим службам» Промежуточный сервер и файловый сервер в одном домене, и есть помощь администратора домена Настройка делается не в приложении, а на стороне домена. SPN делегируемых служб перечисляют явно
Ограниченное делегирование на основе ресурсов Разрешение на делегирование держат на стороне того, кому делегируют, то есть на учётной записи файлового сервера Когда нужно пересечь домены или инициативу берёт администратор ресурса Это механизм Windows Server 2012 и новее. Место настройки противоположно классическому ограниченному делегированию
Явно передать учётные данные Подключаться не от самого пользователя, а от специальной учётной записи с узким назначением Делегирование настроить нельзя, или «это именно этот пользователь» не является бизнес-требованием Появляется задача хранения учётных данных. См. таблицу в главе 10
Не ретранслировать Доступ к файловому серверу отдать служебной учётной записи, а авторизацию оставить приложению Бизнес-правила живут на стороне приложения ACL перестаёт быть финальным решением. Важно заранее спроектировать журнал аудита
Пустить клиента напрямую Не гонять доступ через сервер, а открывать файловый сервер с клиентского ПК Достаточно открыть общую папку из интерфейса Централизованно управлять доступом и собирать логи на сервере уже не получится

Порядок решения такой: сначала ответьте, действительно ли нужно идти на файловый сервер именно с Windows-правами самого пользователя. Если да — переходите к проектированию делегирования; если нет — склоняйтесь к схеме без ретрансляции. Настройка делегирования не заканчивается на разработчике приложения, поэтому как только стало ясно, что оно нужно, разумнее сразу обсудить это с администратором домена.

С другой стороны, в зависимости от бизнес-требований обращаться к файловому серверу именно с Windows-правами самого пользователя может быть и не нужно.

В таком случае проще следующий подход:

Аутентификация и авторизация пользователя выполняются на стороне приложения
  ↓
Доступ к файловому серверу выполняется от специальной служебной учётной записи
  ↓
В журнал операций записываются ID пользователя и целевой файл

Это подход, в котором финальное решение принимает не «ACL операционной системы», а «авторизация приложения».

Какой из вариантов правильный, зависит от бизнес-требований.

Но если не зафиксировать явно, какой из подходов выбран, олицетворение, делегирование, ACL и авторизация приложения перемешиваются, и в этом становится сложно разобраться.

19. Время жизни хендлов токенов

Токен — это хендл объекта ядра, поэтому его нужно закрывать сразу, как только он становится не нужен.

В .NET базовый подход — использовать SafeAccessTokenHandle и управлять его временем жизни через using.

using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);

string result = WindowsIdentity.RunImpersonated(token, () =>
{
    return File.ReadAllText(path);
});

Плохой пример:

// Плохой пример: хранить токен глобально бесконечно долго
private static SafeAccessTokenHandle? _cachedToken;

Долгое хранение токена приводит к таким проблемам:

  • утечки хендлов;
  • становится непонятно, какая операция использует какой токен;
  • сложно отследить согласованность с отключением учётной записи или изменением прав;
  • проектирование скатывается к долгому хранению учётных данных;
  • это трудно объяснить при аудите.

Принцип таков:

Получать токен только тогда, когда он нужен
  ↓
Использовать в минимально необходимой области
  ↓
Обязательно закрывать

Конечно, в зависимости от стоимости аутентификации и эксплуатационных требований кеширование иногда стоит рассмотреть. Но и в этом случае в проектирование нужно закладывать срок действия, уничтожение, изменение учётной записи, журнал аудита и обработку изменения прав.

20. Что нужно фиксировать в журнале аудита

Для операций с олицетворением важно и продумать логирование.

Если сохранять хотя бы информацию из этой таблицы, последующее расследование становится намного проще.

Поле Пример
Запросивший пользователь DOMAIN\alice
Учётная запись выполняющего процесса DOMAIN\svc-app
Олицетворённая учётная запись DOMAIN\alice или специальная учётная запись
Целевой ресурс Путь к файлу, имя общей папки, ключ реестра и т. п.
Операция Read, Write, Delete, CreateProcess и т. п.
Результат Success, AccessDenied, Timeout, UnexpectedError
Код ошибки Код ошибки Win32, HRESULT, тип исключения
Область олицетворения В каком методе и для какой операции выполнялось олицетворение

При этом кое-что в лог выводить нельзя.

  • пароли;
  • значения токенов доступа;
  • заголовки аутентификации;
  • билеты Kerberos и сами учётные данные;
  • содержимое файлов с персональными данными.

Цель журнала — потом суметь проследить, «по чьему запросу, от какой учётной записи, что было предпринято и чем это закончилось: успехом или неудачей».

Сами учётные данные фиксировать не нужно.

21. Частые антипаттерны

Соберём опасные реализации, которые чаще всего встречаются вокруг олицетворения.

21.1 Олицетворение всего приложения целиком

WindowsIdentity.RunImpersonated(token, () =>
{
    RunEntireApplication();
});

Если олицетворять всё приложение целиком, становится непонятно, какая операция выполняется с какими правами.

Олицетворение стоит ограничивать необходимым вводом-выводом и конкретными вызовами API.

21.2 Откат без finally

ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();

Это опасно, потому что при исключении откат не произойдёт.

Всегда используйте try / finally или RunImpersonated.

21.3 Fire-and-forget во время олицетворения

WindowsIdentity.RunImpersonated(token, () =>
{
    _ = Task.Run(DoWorkAsync);
});

Вы выходите из области олицетворения раньше, чем операция закончится. При такой записи отправленная задача (как в главе 14) продолжает работать олицетворённой, поэтому область олицетворения в коде и фактическая область расходятся. К тому же никто не делает await, поэтому при сбое исключение не всплывает. Если олицетворять напрямую через Win32, картина обратная: работа идёт от учётной записи процесса.

Если операции нужно олицетворение, используйте await внутри RunImpersonatedAsync.

21.4 Определение успеха только по имени пользователя

Console.WriteLine(WindowsIdentity.GetCurrent().Name);

Даже если имя пользователя совпадает с ожидаемым, уровня олицетворения или прав может не хватать.

Проверяйте также ImpersonationLevel, ACL целевого ресурса, тип входа и настройки сетевого делегирования.

21.5 Использование администраторской учётной записи как «удобной» для олицетворения

Подход «эта операция не должна падать, поэтому олицетворяем администраторскую учётную запись» опасен.

Безопаснее подготовить специальную учётную запись с минимально необходимыми правами и сузить набор допустимых операций.

21.6 Хранение пароля в файле конфигурации

{
  "UserName": "DOMAIN\\admin",
  "Password": "P@ssw0rd!"
}

Этого следует избегать.

Если приходится работать с учётными данными, используйте механизм, подходящий вашему окружению: хранилище секретов, Диспетчер учётных данных Windows, DPAPI, облачный Key Vault или средства управления секретами вашей эксплуатационной платформы.

21.7 Смешение запуска процесса и доступа к файлу в одну задачу

Если нужен только доступ к файлу, зачастую достаточно токена олицетворения.

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

Если вы используете CreateProcessAsUser или CreateProcessWithTokenW, стоит рассматривать это как отдельную от олицетворения задачу проектирования.

22. Что проверять в тестах

Если проверять код с олицетворением только в своём локальном окружении с правами администратора, легко что-то упустить.

Как минимум стоит подготовить такие тестовые сценарии:

Сценарий Что проверить
Пользователь с правами Может читать / записывать целевой файл
Пользователь без прав Корректно завершается ошибкой Access denied
Несуществующий пользователь Обрабатывается как ошибка аутентификации
Неверный пароль Завершается ошибкой, не раскрывая секреты в логах
Сетевая общая папка Проверена разница поведения между локальным ресурсом и UNC
Асинхронная обработка Работает в ожидаемой области и после await
Возникновение исключения Олицетворение гарантированно снимается
Параллельные запросы Олицетворения разных пользователей не смешиваются
Запуск как служба Работает под реальной служебной учётной записью, а не под интерактивным входом разработчика

Особенно важны эти два момента:

Успешное выполнение
Отказ именно тогда, когда он должен произойти

Для кода с олицетворением тестируйте не только успешные сценарии, но и то, что пользователь без прав гарантированно получает отказ.

Если операция, которая должна быть отклонена, вдруг проходит успешно, это может говорить об ошибке в проектировании олицетворения или авторизации.

23. Чек-лист для реализации

До и после реализации стоит пройтись по этому чек-листу.

Аспект Что проверить
Цель Можете ли вы объяснить, зачем нужно олицетворение
Альтернативы Рассмотрели ли вы, не обойтись ли служебной учётной записью или авторизацией приложения
Область Минимальна ли область олицетворения
Откат Гарантированно ли выполняется откат при исключениях
Асинхронность Ожидается ли (await) всё до завершения внутри RunImpersonatedAsync
Токены Не путаете ли вы первичный токен с токеном олицетворения
Уровень олицетворения Учитываете ли разницу между Identification и Impersonation / Delegation
Сеть Проверили ли необходимость UNC, риск двойного прыжка, делегирования Kerberos
UAC Не путаете ли вы членство в администраторах с фактическим повышением прав
Секреты Не хранится ли пароль в открытом виде
Хендлы Закрывается ли SafeAccessTokenHandle через using
Логирование Можно ли проследить пользователя, олицетворённую учётную запись, ресурс и результат
Тесты Проверены ли случаи с правами / без прав / исключения / параллельная работа

Если по этому чек-листу возникает много вопросов, лучше пересмотреть проектирование ещё до написания кода.

24. Когда это уместно

Токены олицетворения — мощный инструмент, но не всегда стоит хвататься за них в первую очередь.

Если разложить проектирование по сценариям использования, сомнений станет меньше.

24.1 Когда нужно использовать ACL операционной системы напрямую

Если ACL файлового сервера или общих папок — это основа бизнес-правил, и приложение должно им подчиняться, олицетворение самого пользователя имеет смысл.

Доступ от имени самого пользователя
  ↓
Финальное решение принимают ACL Windows

В этом случае проектировать нужно целиком: аутентификацию Windows, уровень олицетворения, делегирование и сетевую конфигурацию.

24.2 Когда авторизацию нужно выполнять на стороне приложения

Если бизнес-правила находятся на стороне приложения, а файловый сервер или БД — под его управлением, зачастую понятнее обращаться через служебную учётную запись, а авторизацию выполнять внутри приложения.

Аутентифицируем пользователя
  ↓
Авторизуем в приложении
  ↓
Обращаемся к ресурсу от служебной учётной записи
  ↓
Фиксируем ID пользователя в журнале аудита

В этом подходе олицетворение не используется, зато на первый план выходят логика авторизации приложения и журнал аудита.

24.3 Когда нужны административные операции

Чаще безопаснее не выполнять административные операции напрямую с токеном пользователя, а завести отдельную службу или API для таких операций, где будут выполняться авторизация, проверка входных данных, аудит и откат изменений.

Клиент
  ↓
Запрос к административному API
  ↓
Административный API выполняет авторизацию
  ↓
Операция выполняется с минимально необходимыми правами
  ↓
Журнал аудита

«Просто олицетворить администратора для начала» выглядит проще в краткосрочной перспективе.

Но в долгосрочной перспективе это усложняет аудит, расследование инцидентов, изменение прав и проверки безопасности.

25. Заключение

Токены олицетворения Windows — важный механизм, чтобы корректно пользоваться системой управления правами Windows.

Однако это как раз та область, где код, написанный без понимания сути, легко выглядит рабочим, оставаясь при этом опасным.

Соберём ключевые моменты, которые нужно держать в голове:

  • токен доступа представляет контекст безопасности: пользователя, группы, права и прочее;
  • у процесса есть первичный токен;
  • токен олицетворения присоединяется в основном к потоку и используется при проверке доступа;
  • олицетворение — это не повышение прав;
  • у первичного токена и токена олицетворения разное назначение;
  • уровень олицетворения определяет, можно ли только идентифицировать клиента, действительно получить доступ или делегировать права удалённым системам;
  • при олицетворении через Win32 API обязательно вызывайте RevertToSelf в try / finally;
  • в .NET выражайте небольшую область через WindowsIdentity.RunImpersonated / RunImpersonatedAsync;
  • при использовании LogonUser обращайте внимание на управление учётными данными и тип входа;
  • для сетевых общих папок важно не только олицетворение, но и делегирование Kerberos, и проектирование служебных учётных записей;
  • управляйте хендлами токенов через SafeAccessTokenHandle и using;
  • тестируйте не только успешные сценарии, но и те, что должны заканчиваться отказом.

В реализации с токенами олицетворения важен не сам факт «сработало от имени другого пользователя», а то, чтобы вы могли объяснить всё перечисленное ниже.

Какая операция
по чьему запросу
от какой учётной записи
в каких пределах выполнена
где произошёл откат
и как зафиксированы успех и неудача

Если вы можете разложить всё это по полочкам, олицетворение перестаёт быть пугающим механизмом.

Оно становится практичным инструментом, который помогает задействовать ACL Windows, служебные учётные записи, существующие файловые серверы и доменную инфраструктуру компании.

Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое токен олицетворения в Windows?
Токен олицетворения — это токен, который присоединяется к потоку, чтобы этот поток проходил проверки доступа в другом контексте безопасности. Весь процесс при этом не становится другим пользователем: проверки доступа к файлам, реестру и другим объектам с правами этого пользователя проходит только олицетворяющий поток. Это не «магия, превращающая вас в администратора», то есть не повышение прав, а механизм, который позволяет проверять доступ лишь для части операций серверного процесса с правами клиентского пользователя.
Чем первичный токен отличается от токена олицетворения?
Первичный токен представляет контекст безопасности процесса и используется для запуска процессов, например через CreateProcessAsUser. Токен олицетворения нужен, чтобы поток работал в другом контексте безопасности: он появляется при ImpersonateLoggedOnUser или при олицетворении клиента через Named Pipe. Если нужно запустить процесс, как правило требуется именно первичный токен. Если под рукой есть только токен олицетворения, из него делают первичный через DuplicateTokenEx. Если эти два токена путать, легко застрять на ошибках вроде Access denied.
Почему возникает Access denied, хотя олицетворение вроде бы выполняется?
Проверять нужно сразу несколько вещей. Внутри области олицетворения смотрите не только на Name из WindowsIdentity.GetCurrent(), но и на ImpersonationLevel — уровень должен быть не Identification, а хотя бы Impersonation. Проверьте ACL целевого ресурса, не ушёл ли фактический ввод-вывод за пределы области олицетворения или в другую задачу, не столкнулись ли вы с проблемой двойного прыжка (double hop), когда локальный доступ проходит, а UNC — нет, и не влияет ли на ситуацию не повышенный токен из-за UAC. Даже если имя пользователя совпадает с ожидаемым, прав может не хватать.
Как безопасно реализовать олицетворение в .NET?
Базовый подход — WindowsIdentity.RunImpersonated / RunImpersonatedAsync, а область олицетворения держать внутри лямбда-выражения. Асинхронную работу, которой нужно олицетворение, ожидайте через await внутри RunImpersonatedAsync и не запускайте fire-and-forget задачи из этой области. Если олицетворяете через Win32 API, обязательно вызывайте RevertToSelf в блоке try/finally. Область олицетворения сужайте до необходимого ввода-вывода, а хендлы токенов ведите через SafeAccessTokenHandle и using.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог