Минимальный чек-лист безопасности при разработке Windows-приложений
· Обновлено: · Го Комура · Разработка Windows, Безопасность, Проектирование, C# / .NET, Win32
История изменений (4 обновлений, последнее 31 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Текст статьи исправлен, чтобы соответствовать японскому оригиналу.
- Восстановлены пропущенные источники в списке литературы. Утверждения статьи не менялись.
- Исправлен URL CryptProtectData в списке источников. Утверждения статьи не менялись.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619679)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Минимальный чек-лист безопасности при разработке Windows-приложений. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619679 https://comcomponent.com/ru/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- DOI (последняя версия)
- 10.5281/zenodo.21619679
- DOI (эта версия)
- 10.5281/zenodo.21619680
Содержимое файла совпадает с чек-листом перед релизом из главы 4 (8 категорий, 32 пункта). Отличия только в том, что есть колонки Status и Notes, и что японский и английский лежат на двух листах: Checklist-ja и Checklist-en. Читать по ходу статьи удобнее главу 4; раздавать как запись ревью — Excel-версию.
Разговор про безопасность Windows-приложений быстро раздувается. Zero Trust, EDR, SBOM (Software Bill of Materials, перечень программных компонентов), работа с сертификатами, управление уязвимостями. Всё это важно, но на практике ещё до этого есть база, которую не хочется пропускать.
Особенно в приложениях ниже закрывать дыры в базе выгоднее, чем сразу строить «продвинутую защиту».
- десктопные приложения на WPF / WinForms / WinUI
- Win32-приложения на C++ / C#
- интеграция с оборудованием, файловый обмен, подключения к БД, внутренние утилиты
- бизнес-приложения с автообновлением
- конфигурации со службами Windows или вспомогательными EXE
В разработке Windows-приложений реалистичнее сначала не оставлять явно опасных дыр, чем пытаться сразу довести всё до идеала. Ниже — минимальные пункты, которые не стоит пропускать, в порядке проектирования, реализации, распространения и эксплуатации, в виде, удобном для проверки.
flowchart TB
accTitle: Как устроена эта статья
accDescr: Сначала закрываем пробелы в базе, а не продвинутую защиту, и смотрим пункты в порядке проектирования, реализации, распространения и эксплуатации.
adv1["Продвинутая защита (Zero Trust и т.п.)"] -.->|"сначала"| base1["Закрыть пробелы в базе"]
base1 --> o1["Проектирование"]
o1 --> o2["Реализация"]
o2 --> o3["Распространение"]
o3 --> o4["Эксплуатация"]
Рис. 1: Сначала база, потом продвинутая защита; смотрим проектирование, реализацию, распространение и эксплуатацию.
1. Сначала вывод
- Первое, что не стоит пропускать: не запрашивать лишние права администратора, подписывать код, не хранить секреты в открытом виде, не отключать проверку сертификатов.
- У Windows-приложения сам дистрибутив — поверхность атаки. Безопаснее смотреть на всё: EXE / DLL / MSI / MSIX / модуль автообновления.
ServerCertificateValidationCallback => true, plaintext-строки подключения, небрежная загрузка вродеLoadLibrary("foo.dll")и SQL через конкатенацию строк — то, чего стоит избегать даже на минимальной планке.- Если права администратора нужны только части операций, безопаснее не повышать права всего приложения, а вынести эту часть в отдельный EXE или службу.
- Для приложения, которое ставят на Windows, лучше по умолчанию закладывать подпись + метку времени. Это не только доверие пользователей: проще ловить подмену и объяснять это на эксплуатации.
- Секреты на диске в зависимости от задачи закрывают DPAPI / ProtectedData или Credential Locker. Как минимум стоит уйти от plaintext в
appsettings.json. - «Чем больше логов, тем лучше» — нет. Если оставлять токены, пароли, строки подключения, персональные данные, полное тело запроса как есть, сам лог становится источником инцидента.
Минимальная безопасность — не про то, чтобы добавить особую функцию, а про то, чтобы не оставлять опасное поведение по умолчанию и небрежную реализацию.
flowchart TB
accTitle: Что имеется в виду под минимальной безопасностью
accDescr: Минимальная безопасность — не добавление особых функций, а отказ оставлять опасное поведение по умолчанию и небрежную реализацию.
add1["Добавить особую функцию"] -.->|"не в этом смысл минимума"| goal1["Минимальная безопасность"]
rm1["Не оставлять опасные умолчания и небрежный код"] -->|"именно это"| goal1
Рис. 2: Минимальная планка — не новые функции, а отказ оставлять опасные умолчания и небрежный код.
Карта знаний этой статьи
Статья сводит в чек-лист минимальные меры безопасности, от которых не стоит отказываться перед выпуском, для бизнес-приложений Windows на WPF, WinForms, WinUI, C++ и C#. В центре: не ставить всему приложению requireAdministrator, а выносить только нужные операции в отдельный процесс или службу; подписывать дистрибутив с меткой времени и не распространять без подписи и не отключать проверку сертификатов на постоянной основе; не класть секреты в открытую конфигурацию, а защищать их, например, через DPAPI. Дополнительно: конкатенация строк в SQL ведёт к SQL-инъекции, её предотвращают плейсхолдеры; загрузка DLL только по имени открывает hijacking порядка поиска; вывод конфиденциальных данных в лог делает сам лог каналом утечки.
flowchart LR
accTitle: Карта знаний минимального чек-листа безопасности Windows-приложений
accDescr: Схема связей между минимальными пунктами безопасности: обращение с правами администратора, проверка подписи кода и обновлений, защита секретов, риски ввода и выполнения вроде SQL-инъекции и загрузки DLL, утечка конфиденциальных данных через логи
admin_rights["права администратора"]
code_signing_cert["сертификат подписи кода"]
app_secrets["секреты приложения"]
execution_level_asinvoker["asInvoker (уровень запуска)"]
execution_level_requireadministrator["requireAdministrator (уровень запуска)"]
desktop_app["настольное приложение интерактивного пользователя"]
admin_privilege_separation["отделение операций с правами администратора"]
dll_search_order_hijacking["подмена DLL через порядок поиска"]
unvalidated_dll_loading["загрузка DLL только по имени"]
unsigned_binary["неподписанный бинарный файл"]
code_signing_timestamp["метка времени подписи кода"]
certificate_expiry["истечение сертификата"]
update_integrity_verification["проверка подписи и хеша обновления"]
unverified_update_application["применение непроверенного обновления"]
certificate_pinning["пиннинг сертификата (certificate pinning)"]
certificate_validation_bypass["постоянный обход проверки сертификата"]
certificate_revocation_check["проверка отзыва сертификата"]
sql_string_concatenation["конкатенация SQL-строк"]
sql_injection["SQL-инъекция"]
prepared_statement["плейсхолдер (prepared statement)"]
secret_logging["запись секретов в журнал"]
secret_log_exposure["раскрытие секретов через журналы"]
dpapi["DPAPI"]
plaintext_secret_storage["хранение секретов в открытом виде"]
windows_service["служба Windows"]
admin_rights -->|"настраивается"| execution_level_asinvoker
admin_rights -->|"настраивается"| execution_level_requireadministrator
execution_level_requireadministrator -->|"не рекомендуется"| desktop_app
admin_privilege_separation -->|"рекомендуется для"| execution_level_requireadministrator
execution_level_requireadministrator -.->|"может вызвать"| dll_search_order_hijacking
unvalidated_dll_loading -->|"может вызвать"| dll_search_order_hijacking
code_signing_cert -->|"предотвращает"| unsigned_binary
code_signing_timestamp -->|"снижает"| certificate_expiry
update_integrity_verification -->|"предотвращает"| unverified_update_application
code_signing_cert -->|"рекомендуется для"| update_integrity_verification
certificate_pinning -->|"рекомендуется для"| certificate_validation_bypass
certificate_pinning -.->|"требует"| certificate_revocation_check
certificate_validation_bypass -->|"не рекомендуется"| desktop_app
sql_string_concatenation -->|"может вызвать"| sql_injection
prepared_statement -->|"предотвращает"| sql_injection
secret_logging -->|"может вызвать"| secret_log_exposure
dpapi -->|"рекомендуется для"| plaintext_secret_storage
app_secrets -.->|"требует"| dpapi
admin_privilege_separation -.->|"использует"| windows_service
code_signing_cert -.->|"требует"| code_signing_timestamp
prepared_statement -->|"рекомендуется для"| desktop_app
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Охват статьи и что значит «минимум»
2.1. Что входит в статью
Речь о таких Windows-приложениях.
- десктопные приложения на WPF / WinForms / WinUI
- Win32-приложения на C++ / C#
- внутренние утилиты, интеграция с оборудованием, мониторинг
- конфигурации со вспомогательными EXE, службами Windows, апдейтерами
- бизнес-ПО, которое распространяют как EXE / MSI / MSIX
«Минимум» здесь — не финальное состояние, которое проходит аудит, а пункты, без которых инцидент случается на ровном месте.
flowchart TB
accTitle: Что в этой статье значит «минимум»
accDescr: Минимум здесь — не финальное состояние для аудита, а пункты, без которых инцидент случается на ровном месте.
au1["Финальное состояние, проходящее аудит"] -.->|"здесь не это"| mn1["«Минимум» в этой статье"]
ac1["Пункты, без которых случается инцидент"] -->|"именно это"| mn1
Рис. 3: «Минимум» — не аудит-ready финал, а то, без чего инцидент случается сам.
Зафиксируем и предпосылки примеров кода. Примеры на C# рассчитаны на .NET 8 и новее, примеры на C++ — на Win32 API. На .NET Framework 4.8 идея та же, но рекомендуемый способ записи в части мест уже другой. Типичный пример — ServicePointManager в 3.6: в новом коде исходят из IHttpClientFactory и HttpClient. Когда перечитываете старый код, смотрите именно сюда.
2.2. Что не входит
Есть и то, что мы сознательно выносим за основной фокус.
- Zero Trust на всю компанию
- комплексная эксплуатация EDR / SIEM / DLP / MDM
- детальный hardening драйверов ядра
- криптосхема с нуля
- продвинутый threat analysis и процедуры форензики
То есть это не «масштабная программа безопасности организации», а базовая линия, которую разработчик Windows-приложения ещё может сам закрыть перед релизом.
flowchart TB
accTitle: Что входит и что нет
accDescr: Масштабные меры на всю организацию остаются за кадром; статья про базовую линию, которую разработчик ещё может закрыть сам перед релизом.
org1["Масштабные меры на всю организацию"] -.->|"в этой статье нет"| sc1["Охват статьи"]
dev1["Базовая линия, которую разработчик ещё может закрыть сам"] -->|"об этом"| sc1
Рис. 4: Не корпоративная программа, а база, которую разработчик закрывает сам перед релизом.
3. Чек-лист, с которого начать
До тонких споров положим таблицу, по которой видно целое. Уже по ней понятно, куда смотреть.
3.1. Общая картина
| Что проверить | Минимальные действия | Типичная ошибка |
|---|---|---|
| Права выполнения | По умолчанию asInvoker; операции, которым нужно повышение, вынести отдельно |
Делать всё приложение requireAdministrator |
| Доверие к дистрибутиву | Подписывать EXE / DLL / MSI / MSIX и ставить метку времени | Раздавать без подписи |
| Обновление | Зафиксировать источник, ловить подмену через HTTPS и проверку подписи | Скачать по HTTP и сразу перезаписать |
| Секреты | Не класть секреты в исходники и plaintext-конфиг; DPAPI / Credential Locker и т.п. | API-ключи и строки подключения в конфиге открытым текстом |
| Сеть | HTTPS, не отключать проверку сертификатов | Постоянно пропускать проверку через return true |
| Внешний ввод | Проверять всё: SQL, файлы, IPC, URI, CSV, JSON и остальное | Пропускать без проверки, потому что «это внутренний инструмент» |
| Загрузка DLL | Абсолютный путь, SetDefaultDllDirectories, безопасный порядок поиска |
LoadLibrary("foo.dll") из текущего каталога |
| Логи | Маскировать токены, пароли, PII; разделять ошибки для пользователя и для себя | Показывать и сохранять детали исключений и строки подключения как есть |
| Зависимости | Постоянно обновлять SDK, NuGet, VC++ runtime, OSS-зависимости | Зафиксировать версии на годы и не следить за уязвимостями |
3.2. Права: по умолчанию asInvoker
В Windows-приложении это первое, что стоит пересмотреть. Если всё приложение работает от администратора, баги, подмена DLL, неверное чтение конфигурации и дыры во внешнем вводе исполняются уже с этими высокими правами.
flowchart TB
accTitle: Чем опасно поднимать права всего приложения
accDescr: Если всё приложение работает от администратора, баги, подмена DLL, неверное чтение конфигурации и дыры во вводе исполняются с этими же высокими правами.
all1["Всё приложение от администратора"] --> bug1["Баги"]
all1 --> swp1["Подмена DLL"]
all1 --> inp1["Ошибка чтения конфига / дыра во вводе"]
bug1 --> pw1["Исполняется уже с высокими правами"]
swp1 --> pw1
inp1 --> pw1
Рис. 5: Если поднять права всего приложения, все его дыры исполняются уже с администратором.
Базовая стратегия такая.
- Обычное UI-приложение —
asInvoker - Операции, которым нужны права администратора, вынести в отдельный процесс или службу
- Повышать права только в нужный момент
- Ввод, который уходит во вспомогательный EXE или службу, тоже проверять
Если десктопное приложение в обычной работе только смотрит и правит данные, а права администратора нужны лишь для установки или смены настроек брандмауэра, безопаснее не ставить requireAdministrator на всё приложение, а свести повышение в broker.
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
«Проще, если оно просто работает от администратора» — обычно потом аукается. Держите минимальные права и вырезайте только те операции, без которых никак: радиус инцидента заметно сужается.
flowchart TB
accTitle: Как вынести только те операции, которым нужно повышение
accDescr: Обычный UI работает как asInvoker, операции с правами администратора уходят в broker (отдельный EXE или служба), повышение — только в нужный момент.
ui1["UI-приложение (asInvoker)"] -->|"запрос только в нужный момент"| br1["broker (отдельный EXE / служба)"]
br1 --> el1["Выполняется только то, чему нужно повышение"]
ui1 -.-> vd1["Ввод в broker тоже проверяется"]
Рис. 6: Обычная работа на asInvoker, повышение — только в broker; радиус инцидента меньше.
3.3. Подписывать бинарники и установщик
В Windows решает доверие к дистрибутиву. Пользователь трогает не исходники, а EXE, DLL, MSI, MSIX, апдейтер. Без подписи слабеют и объяснение на эксплуатации, и обнаружение подмены, и спокойствие при раздаче.
flowchart TB
accTitle: Пользователь трогает дистрибутив, не исходники
accDescr: Пользователь работает с EXE, DLL, установщиком и апдейтером; без подписи слабеют и обнаружение подмены, и объяснение на эксплуатации.
us1["Что трогает пользователь"] --> bin1["EXE / DLL"]
us1 --> pkg1["MSI / MSIX"]
us1 --> upd1["Апдейтер"]
bin1 --> ns1["Без подписи слабее и обнаружение подмены, и объяснение"]
pkg1 --> ns1
upd1 --> ns1
Рис. 7: Поверхность атаки — сам дистрибутив; без подписи не на чем строить доверие.
На минимуме стоит посмотреть сюда.
- Подписывать EXE / DLL / MSI / MSIX
- Подписывать не только установщик, но и вспомогательные бинарники обновления
- Ставить метку времени
- Срок сертификата и порядок его продления включить в процедуру релиза
Подпись без метки времени особенно часто ломает проверку после истечения сертификата. «Подписали — и хватит» недостаточно: стабильнее заложить в процедуру релиза связку подпись + метка времени.
flowchart TB
accTitle: Подпись и метка времени
accDescr: Подпись без метки времени после истечения сертификата часто не проходит проверку; связка подпись плюс метка времени в процедуре релиза стабильнее.
sg2["Только подпись"] --> tr1["После истечения сертификата проверка часто ломается"]
ts1["Подпись + метка времени"] --> st2["После истечения проверять проще"]
ts1 -.-> fl1["Заложить в процедуру релиза"]
Рис. 8: Не останавливайтесь на подписи: метку времени тоже внесите в процедуру релиза.
Для MSIX подпись пакета — обязательное условие. При раздаче MSI / EXE тоже стоит подписывать хотя бы сам установщик и основные исполняемые файлы.
3.4. Зафиксировать канал обновления и ловить подмену
В современном Windows-приложении канал обновления живёт дольше первой установки. Если сделать его небрежно, даже аккуратно написанное приложение упрётся в апдейтер как в самое слабое место.
flowchart TB
accTitle: Канал обновления живёт дольше установки
accDescr: Первая установка бывает один раз, канал обновления используется долго; если сделать его небрежно, апдейтер становится самым слабым местом приложения.
ins1["Первая установка"] -.->|"один раз"| ap1["Срок жизни приложения"]
up2["Канал обновления"] -->|"используется долго"| ap1
up2 --> wk2["Небрежно — становится самым слабым местом"]
Рис. 9: Канал обновления живёт дольше установки; небрежная реализация делает его главной дырой.
В обновлениях на минимуме стоит продумать эти пять пунктов.
- Файлы обновления забирать только по HTTPS
- Проверять подпись или хеш скачанного обновления
- Не давать бесконечно подменять URL источника через код или настройки
- Подписывать сам модуль обновления
- Заранее решить откат и восстановление при сбое
Если можно взять MSIX + App Installer, механизм обновления проще опереть на ОС. Если апдейтер свой, нужно проверить и безопасность канала, и подлинность дистрибутива. HTTPS закрывает «путь», но не отвечает на вопрос «этот файл действительно наш».
flowchart TB
accTitle: Две проверки в обновлении
accDescr: В своём апдейтере нужны и HTTPS для канала, и проверка подписи или хеша скачанного обновления, чтобы убедиться, что файл ваш.
dl1["Забрать обновление по HTTPS"] --> vf1["Проверить подпись или хеш"]
vf1 --> ap2["Применять только то, что прошло проверку"]
dl1 -.-> lim1["HTTPS закрывает только канал"]
vf1 -.-> own1["Убедиться, что это наш выпуск"]
Рис. 10: HTTPS не отвечает на вопрос о подлинности; применять только после проверки подписи или хеша.
3.5. Не класть секреты в исходники и plaintext-конфиг
На практике именно здесь чаще всего случается инцидент. Из соображений «это внутренний инструмент» или «мы всё равно просто раздаём exe» строки подключения, API-ключи, учётные данные общей папки и общие токены оказываются в исходниках или конфиге.
На минимуме так хранить не стоит.
- API-ключ прямо в исходниках
- Пароль открытым текстом в
appsettings.jsonилиapp.config - Строка подключения, попавшая в репозиторий
- Схема, где ключ расшифровки лежит рядом с шифротекстом
- Одни и те же фиксированные учётные данные на всех пользователей
Реалистичные варианты для Windows-приложения обычно сводятся к этим четырём.
- Нужно сохранить учётные данные Windows Для packaged desktop app / WinUI имеет смысл смотреть Credential Locker
- Нужно зашифровать секрет локально
Для Win32 / .NET — DPAPI /
ProtectedData - К цели можно ходить через Windows-аутентификацию или integrated authentication Если можно, пусть приложение вообще не хранит пароль
- Секретами можно управлять в облаке или на сервере Предпочитайте схему, в которой долгоживущий секрет не зашит в клиент
В C# уже одно использование DPAPI заметно лучше plaintext. Если расписать сохранение и чтение целиком, получается так.
// C# / .NET 8. ProtectedData работает только на Windows;
// в .NET нужен NuGet-пакет System.Security.Cryptography.ProtectedData.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SecretStore
{
// То же значение нужно и при расшифровке. null тоже сработает, но с entropy безопаснее.
private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];
public static void Save(string path, string secretText)
{
byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
plaintext,
Entropy,
DataProtectionScope.CurrentUser);
// Сырые байты в файл можно, но для конфига удобнее Base64.
File.WriteAllText(path, Convert.ToBase64String(ciphertext));
CryptographicOperations.ZeroMemory(plaintext);
}
public static string Load(string path)
{
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));
// Если пользователь или entropy не те же, что при сохранении, будет CryptographicException.
byte[] plaintext = ProtectedData.Unprotect(
ciphertext,
Entropy,
DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plaintext);
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
}
Со стороны вызова это выглядит так.
string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SampleApp",
"token.dat");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);
Важно не «зашифровали — значит безопасно», а то, кто может расшифровать, и это нужно решить в архитектуре.
CurrentUser и LocalMachine значат очень разное. CurrentUser расшифрует только тот пользователь, который сохранил данные; LocalMachine — любой на этой машине. Если служба крутится под другой учёткой или пользователей переключают, не решив это заранее, потом упрётесь либо в «не читается», либо в «читается у всех».
flowchart TB
accTitle: Кто может расшифровать данные в DPAPI
accDescr: При CurrentUser расшифрует только пользователь, который сохранил данные; при LocalMachine — любой на этой машине. Кто может расшифровать, нужно решить заранее.
dc1["Решить в архитектуре, кто может расшифровать"] -->|"CurrentUser"| cu1["Только пользователь, который сохранил"]
dc1 -->|"LocalMachine"| lm1["Любой на этой машине"]
dc1 -.-> lt1["Не решить заранее — потом упрётесь"]
Рис. 11: У DPAPI область расшифровки задаёт scope; сначала решите, кто может расшифровать.
Ещё: ключ DPAPI лежит в профиле пользователя, и в документации прямо сказано, что если профиль не загружен (например, во время impersonation), расшифровка падает. Если думаете вызывать DPAPI из службы, проверьте и это.
flowchart TB
accTitle: Ключ DPAPI и профиль пользователя
accDescr: Ключ DPAPI лежит в профиле пользователя, поэтому если профиль не загружен, например во время impersonation, расшифровка падает.
ky1["Ключ DPAPI"] --> pf1["Лежит в профиле пользователя"]
pf1 -->|"Профиль не загружен (impersonation и т.п.)"| fe1["Расшифровка падает"]
fe1 -.-> sv2["Из службы это нужно проверить отдельно"]
Рис. 12: Ключ DPAPI в профиле; без загруженного профиля расшифровать нельзя.
Для подключения к SQL Server в on-premises первым кандидатом часто бывает Windows-аутентификация.
Если без учётных данных в строке подключения никак, хотя бы держите Persist Security Info=False и не оставляйте их в plaintext-конфиге.
3.6. Сеть — HTTPS по умолчанию, проверку сертификатов не отключать
Лазейка «только на время разработки» так и уезжает в прод. Инциденты вокруг сети почти всегда из этой схемы.
В поставке особенно часто остаётся код или настройки такого вида.
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.DangerousAcceptAnyServerCertificateValidator- Поставка с выключенной проверкой отзыва
- Код под самоподписанный сертификат для разработки остаётся в проде
Минимальная политика простая.
- Прод идёт по HTTPS
- Не пропускать проверку сертификата всегда
- Если ослабление проверки всё же нужно, ограничить его конкретным хостом и сертификатом
- Код обхода для разработки гарантированно выкидывать условиями сборки или настройками
- В .NET помнить и про проверку отзыва
Плохой пример обычно выглядит так.
ServicePointManager.ServerCertificateValidationCallback +=
(_, _, _, _) => true;
На вид удобно, но по сути это близко к «это HTTPS-соединение пройдёт к кому угодно». Без проверки сертификата HTTPS остаётся почти пустой оболочкой.
Запись зависит от того, какой .NET вы берёте за основу
Глобальная настройка через ServicePointManager — это ещё .NET Framework. В новом коде берите HttpClient из IHttpClientFactory, а TLS-настройки кладите в SocketsHttpHandler или HttpClientHandler.
Но оставлять старый API со словами «уже не действует» опасно. В документации Microsoft сказано, что ServicePointManager.ServerCertificateValidationCallback начиная с .NET 9 мапится на RemoteCertificateValidationCallback в SocketsHttpHandler.SslOptions. То есть одна строка => true где-то в стороне может пропустить и трафик HttpClient.
flowchart TB
accTitle: Как старый callback дотягивается до нынешнего трафика
accDescr: ServerCertificateValidationCallback в ServicePointManager начиная с .NET 9 мапится на проверку в SocketsHttpHandler, поэтому одна строка true может пропустить и трафик HttpClient.
old1["Проверочный callback в ServicePointManager"] -->|".NET 9 и новее: мапится"| new1["Проверка на стороне SocketsHttpHandler"]
new1 --> ef1["Действует и на трафик HttpClient"]
ef1 -.-> rk2["Одна строка true может выхолостить всю сеть"]
Рис. 13: Пропуск проверки, повешенный на старый API, через маппинг дотягивается и до HttpClient.
Если проверку всё же нужно ослабить точечно, не ставьте глобальную настройку на весь процесс: замкните её на этот handler.
// C# / .NET 8. Исключение только для конкретного хоста и сертификата.
// Даже «только для разработки» без ограничения цели это то же самое, что выключить проверку глобально.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
// Thumbprint целевого сертификата. Можно читать из настроек.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = true, // Ходить за отзывом. По умолчанию false
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// Допускаем только «этот компьютер не доверяет внутреннему CA».
// Нет сертификата или не совпадает имя хоста — нет
if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
{
return false;
}
// Фиксируем собеседника и сертификат
if (request.RequestUri?.Host != "device.internal.example"
|| certificate is null
|| !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Даже у закреплённого сертификата не принимаем истечение и отзыв.
// Иначе останется путь пользоваться сертификатом, который отозвали из-за утечки ключа
return chain is not null
&& chain.ChainStatus.All(s =>
s.Status is X509ChainStatusFlags.NoError
or X509ChainStatusFlags.UntrustedRoot
or X509ChainStatusFlags.PartialChain);
},
};
using var client = new HttpClient(handler);
ExpectedThumbprint задайте константой или из настроек — это thumbprint целевого сертификата.
Важно не делать вид, что errors и chain.ChainStatus можно не смотреть. Если при совпавшем thumbprint сразу вернуть true, проверка пройдёт и после истечения сертификата, и после отзыва из-за утечки ключа. Пиннинг значит «верим только этому одному», а не «этому одному верим при любых обстоятельствах». В коде выше допускаются только UntrustedRoot и PartialChain (внутренний CA не установлен на этот компьютер); NotTimeValid (истёк) и Revoked (отозван) по-прежнему отказ.
Чтобы смотреть отзыв, нужен CheckCertificateRevocationList = true (по умолчанию false, отзыв не проверяется). Наоборот, если внутренний CA не публикует ни CRL, ни OCSP, соединение отвалятся по RevocationStatusUnknown. Это правильное поведение. Если средства проверки отзыва нет, закройте дыру коротким сроком сертификата или каналом, по которому можно заново раздать значение пиннинга. Самое опасное состояние — «пиннить долгоживущий сертификат, который нельзя отозвать».
flowchart TB
accTitle: Как решать при пиннинге
accDescr: Без ошибок — пропускаем; ошибки кроме цепочки — отказ; сверяем хост и thumbprint; по состоянию цепочки истечение и отзыв тоже отказ.
e0["Смотрим errors"] -->|"None"| pass1["Пропустить"]
e0 -->|"Не ошибка цепочки"| rj1["Отказать"]
e0 -->|"Только ошибка цепочки"| hchk["Сверить хост и thumbprint"]
hchk -->|"Не совпало"| rj1
hchk -->|"Совпало"| cchk["Смотрим состояние цепочки"]
cchk -->|"Истёк, отозван и т.п."| rj1
cchk -->|"Только внутренний CA не установлен"| pass1
Рис. 14: Даже при пиннинге ошибки не игнорируем: истечение и отзыв остаются отказом.
3.7. Весь внешний ввод считать недоверенным
Windows-приложение — не веб, поэтому validation ввода часто делают слабо. На деле точек входа больше, чем кажется.
- Пути к файлам
- CSV / Excel / JSON / XML
- Аргументы командной строки
- named pipe / socket / COM / RPC / gRPC
- Строки, которые уходят в БД
- Значения реестра
- Буфер обмена
- URL / deep link
- Данные от внешнего устройства или SDK
На минимуме не пропускайте эти три пункта.
- SQL всегда параметризовать Не собирать SQL конкатенацией строк.
- Путь нормализовать, потом использовать Путь от пользователя не отдавать напрямую в удаление, перезапись, распаковку.
- При чтении внешнего файла ограничивать размер и проверять формат «Открылось» не значит «безопасно».
На SQL это выглядит так — вот чего избегать.
var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";
Хотя бы на минимуме сводите к такому виду.
using System.Data;
using Microsoft.Data.SqlClient;
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;
«Это внутренний инструмент, вводу можно доверять» — довольно опасная предпосылка. В реальности приходят битые CSV, неожиданные имена файлов, старые данные из БД, опечатки оператора и недоделанный JSON, который написал другой инструмент.
flowchart TB
accTitle: Во внутренний инструмент тоже приходит битый ввод
accDescr: Даже во внутренний инструмент приходят битые CSV, странные имена файлов, старые данные БД, опечатки и недоделанный JSON, поэтому весь внешний ввод нужно проверять как недоверенный.
csv1["Битый CSV"] --> in2["Ввод в приложение"]
fn1["Неожиданное имя файла"] --> in2
hm1["Опечатка / старые данные"] --> in2
in2 --> tr2["Всё проверять как недоверенный ввод"]
Рис. 15: Во внутренний инструмент битый ввод тоже приходит; проверяйте на каждом входе.
3.8. Не оставлять источник загрузки DLL размытым
Это ловушка именно Windows.
Если грузить DLL только по имени, как в LoadLibrary("foo.dll"), порядок поиска может подхватить DLL из непреднамеренного места.
Набор действий известен.
- По возможности указывать абсолютный путь к DLL
- Рано вызывать
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - Явно добавлять каталоги поиска через
AddDllDirectory - Не скармливать результат
SearchPathнапрямую вLoadLibrary - Не полагаться целиком на safe DLL search mode
Для native-кода сильная схема — вызвать это на ранней инициализации процесса.
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
Потом через AddDllDirectory регистрируете только нужные дополнительные каталоги.
«Обычно работает», поэтому это легко не трогать. Но если на месте развёртывания сменился рабочий каталог или в PATH попала DLL другого продукта, всё тихо ломается. Это важно не только для безопасности: так же хорошо предотвращает сбои.
flowchart TB
accTitle: Зафиксировать, откуда грузится DLL
accDescr: Загрузка только по имени может подхватить чужую DLL из порядка поиска; рано вызывайте SetDefaultDllDirectories, явно добавляйте каталоги через AddDllDirectory и по возможности указывайте абсолютный путь.
nm1["Грузить только по имени"] --> pick2["Порядок поиска может взять чужую DLL"]
fix1["Рано вызвать SetDefaultDllDirectories"] --> add2["Явно добавить через AddDllDirectory"]
add2 --> abs1["По возможности абсолютный путь"]
abs1 --> safe1["Источник загрузки больше не размыт"]
Рис. 16: Не грузите по одному имени: явно задайте поиск и зафиксируйте источник.
3.9. Не класть секреты в логи и исключения
Наращивать логи ради расследования сбоев важно. Но логи так же легко становятся свалкой секретов.
В логах на минимуме стоит пересмотреть следующее.
- Не писать в лог пароли, Bearer token, API-ключи
- Не писать строку подключения целиком
- Маскировать персональные данные и тело бизнес-данных
- Детали исключения разделять: экран пользователя и внутренний лог
- Не включать в проде отладочные PII-логи
- Пересмотреть права на каталоги dump и trace
В свежем .NET проще строить логирование с расчётом на redaction. Как минимум откажитесь от «всё в строку и сразу в лог».
Типичные промахи:
- Сохранять тело HTTP request / response целиком
- При ошибке аутентификации писать токен или все заголовки
- Кидать текст исключения прямо в MessageBox
- Класть все секретные логи в ZIP для обслуживания
Ошибки, например, разделяют так.
- Пользователю: «Не удалось подключиться к серверу. Проверьте сетевые настройки и URL.»
- Внутренний лог: хост, тип ошибки TLS, correlation ID, stack trace, число повторов
Уже одно это разделение заметно лучше балансирует утечку и расследование.
flowchart TB
accTitle: Как разделять показ ошибки
accDescr: Пользователю — короткое пояснение, во внутренний лог — хост, тип ошибки, correlation ID, stack trace и прочее для расследования.
er1["Ошибка"] --> usr1["Пользователю: только короткое пояснение"]
er1 --> lg2["Внутренний лог: хост, тип, correlation ID и т.п."]
lg2 -.-> bl1["И утечку сдержать, и расследование сохранить"]
Рис. 17: Разделение экрана и внутреннего лога само по себе лучше балансирует утечку и расследование.
3.10. Не забрасывать библиотеки-зависимости и инструменты разработки
Последний пункт неброский, но эффект большой. Даже аккуратно написанное приложение стоит на дырявом фундаменте, если под ним старый runtime или зависимости с известными уязвимостями.
Смотреть нужно немногое.
- Держать .NET SDK / runtime в поддерживаемых версиях
- Регулярно проверять обновления NuGet / OSS
- Для C++ вести версии распространяемого runtime и внешних DLL
- Включать проверку уязвимостей в чек перед релизом
- Держать smoke test, чтобы обновление зависимостей не ломало приложение незаметно
Здесь самое опасное — «потом сделаем пачкой». Полгода–год простоя, и дельта становится такой, что сама работа по безопасности превращается в тяжёлый проект.
flowchart TB
accTitle: Что бывает, если забросить обновление зависимостей
accDescr: Если откладывать обновление на полгода–год, дельта раздувается, и сама работа по безопасности становится тяжёлым проектом.
pt1["Потом сделаем пачкой"] --> ac2["Полгода–год простоя"]
ac2 --> df1["Дельта становится слишком большой"]
df1 --> hw1["Сама работа превращается в тяжёлый проект"]
rg1["Проверять регулярно"] -.->|"этого можно избежать"| hw1
Рис. 18: Чем дольше не трогать зависимости, тем больше дельта и тем тяжелее потом закрывать дыры.
3.11. Как проверять каждый пункт
Чек-лист работает, только если к нему приложен способ проверки. Ниже — то, что по пунктам 3.2–3.10 реально прогнать перед релизом.
| Что проверить | Как проверить |
|---|---|
| Не требуем ли повышение | В манифесте приложения смотреть requestedExecutionLevel. В исходниках — app.manifest, в дистрибутиве — Sigcheck из Sysinternals или редактор ресурсов |
| Подпись и метка времени | В PowerShell выполнить Get-AuthenticodeSignature .\app.exe и смотреть, что Status равен Valid и есть TimeStamperCertificate. То же по каждому комплектуемому DLL и updater |
| Не выключена ли проверка сертификата | По всему коду искать ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback, CheckCertificateRevocationList |
| Секреты в коде | Искать Password=, ApiKey, Secret, Token, ConnectionString. Не только текущие исходники, но и историю репозитория |
| Сборка SQL | Искать конкатенацию со "SELECT, "INSERT, + и проверять, что путь идёт через Parameters.Add |
| Откуда грузится DLL | В Process Monitor сузить на целевой процесс, фильтры Path ends with .dll и Result is NAME NOT FOUND. Видно, куда и в каком порядке ходили; проверьте, что нет лишних каталогов |
| Известные уязвимости зависимостей | Выполнить dotnet list package --vulnerable --include-transitive |
| Не уходят ли секреты в лог | Прогнать приложение и поискать в логах Bearer , Password, Authorization |
Искать можно и rg (ripgrep), и поиском Visual Studio. Пакетно — так.
Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
Select-Object Path, LineNumber, Line
Главное — оставить запись о самой проверке. Если зафиксировать «искали, нашли 0», на следующем релизе достаточно смотреть дельту.
flowchart TB
accTitle: Зачем записывать саму проверку
accDescr: Даже если перед релизом нашлось 0 совпадений, запись об этом позволяет на следующем релизе смотреть только дельту.
chk2["Прогнать проверку перед релизом"] --> rec1["Записать даже «искали, 0 совпадений»"]
rec1 --> nx1["На следующем релизе достаточно дельты"]
Рис. 19: Если зафиксировать факт проверки, дальше достаточно смотреть дельту.
4. Чек-лист перед релизом
Ниже — форма, которую можно сразу взять как шаблон ревью и решения о выпуске. Пункты, которые на минимуме стоит увидеть перед релизом, сгруппированы по категориям.
4.1. Права, модель выполнения
| Пункт | Отметка | Примечание |
|---|---|---|
Обычный запуск идёт как asInvoker |
□ | |
| Операции с правами администратора вынесены в отдельный EXE / службу и т.п. | □ | |
| Если есть служба, учётная запись выполнения не сильнее необходимого | □ | |
Разведены зоны ответственности %ProgramFiles% и пользовательских данных |
□ |
4.2. Распространение, подпись
| Пункт | Отметка | Примечание |
|---|---|---|
| EXE / DLL / MSI / MSIX / updater подписаны | □ | |
| К подписи добавлена метка времени | □ | |
| Срок сертификата и порядок продления входят в процесс релиза | □ | |
| Есть способ проверить хеш дистрибутива и ловить подмену | □ |
4.3. Обновление
| Пункт | Отметка | Примечание |
|---|---|---|
| Обновления забираются по HTTPS | □ | |
| После скачивания проверяется подпись или хеш | □ | |
| Архитектура затрудняет произвольную подмену URL источника | □ | |
| Есть политика отката или повторов при сбое обновления | □ |
4.4. Секреты
| Пункт | Отметка | Примечание |
|---|---|---|
| Пароли, API-ключи, строки подключения не прописаны в исходниках | □ | |
| Секреты не лежат в plaintext-конфиге | □ | |
| Секреты, которые нужно хранить локально, закрыты DPAPI / Credential Locker и т.п. | □ | |
| Где можно, уходим на Windows-аутентификацию или учётные данные пользователя | □ |
4.5. Сеть
| Пункт | Отметка | Примечание |
|---|---|---|
| Прод идёт по HTTPS | □ | |
DangerousAcceptAnyServerCertificateValidator и => true не остались в поставке |
□ | |
| Учтены проверка отзыва и проверка имени хоста | □ | |
| В прод не попал код или настройки под сертификат для разработки | □ |
4.6. Ввод, доступ к данным
| Пункт | Отметка | Примечание |
|---|---|---|
| SQL параметризован | □ | |
| Для ввода из командной строки, файлов, IPC, URI и т.п. есть лимиты и проверка формата | □ | |
| Операции с путями нормализуются, выход за корень не проходит | □ | |
| Текст исключения не выводится на экран как есть | □ |
4.7. DLL и среда выполнения
| Пункт | Отметка | Примечание |
|---|---|---|
| Источник загрузки DLL задан явно | □ | |
Порядок поиска контролируется через SetDefaultDllDirectories / AddDllDirectory и т.п. |
□ | |
| Загрузка DLL не полагается на текущий каталог или PATH | □ | |
| Известен набор файлов, нужных для динамической загрузки на месте развёртывания | □ |
4.8. Логи, эксплуатация
| Пункт | Отметка | Примечание |
|---|---|---|
| Токены, пароли, PII не уходят в лог | □ | |
| Внутренний лог отделён от сообщений пользователю | □ | |
| Права на каталоги dump / trace / log пересмотрены | □ | |
| Проверяется актуальность SDK и библиотек-зависимостей | □ |
5. Частые ошибки
На практике чаще всего встречаются примерно такие убеждения.
5.1. «Это внутренний инструмент, значит всё в порядке»
Даже во внутреннем инструменте обычное дело — битые файлы, ошибки оператора, принесённые устройства, общие папки, старые DLL, небрежные права. Отсутствие публикации в интернет поверхность атаки не убирает.
5.2. «HTTPS — значит безопасно»
HTTPS важен, но если отключить проверку сертификата, смысл сильно худеет. В раздаче обновлений нужен не только HTTPS, но и проверка подлинности дистрибутива.
5.3. «Зашифровали — значит безопасно»
Если не разведены место ключа, права на расшифровку, граница пользователя и граница машины, одного шифрования мало.
Особенно путаница будет, если значение, защищённое LocalMachine, считать «секретом конкретного пользователя».
5.4. «Больше логов — легче расследовать»
Если логов просто много, а токены и персональные данные льются без разбора, это само становится инцидентом. Если нужна расследуемость, сначала решите, что оставлять, а что скрывать.
5.5. «Запустим от администратора — и всё решится»
Сначала удобно, потом обычно упираетесь в UAC, распространение, поддержку, границы прав, загрузку DLL и места хранения файлов. В долгую стабильнее минимальные права.
flowchart TB
accTitle: Чем кончается «запустим от администратора»
accDescr: Постоянная работа от администратора сначала удобна, потом ломается об UAC, распространение, поддержку, границы прав, загрузку DLL и каталоги; в долгую стабильнее минимальные права.
ez1["Запустим от администратора — и всё решится"] --> ez2["Сначала удобно"]
ez2 --> pain1["Потом тяжело с UAC, распространением, поддержкой"]
ez2 --> pain2["Потом тяжело с границами прав и каталогами"]
lp1["Работать с минимальными правами"] -->|"в долгую стабильнее"| ok2["Этой боли можно избежать"]
Рис. 20: Удобство «всегда администратор» короткое; в долгую стабильнее минимальные права.
6. Примерный порядок приоритетов
Если делать всё сразу тяжело, приоритеты обычно такие.
Критерий — масштаб ущерба при инциденте × дешевизна исправления. Глава 3 идёт по потоку проектирования «права → распространение → реализация → эксплуатация»; здесь порядок «сначала самое опасное», поэтому с главами он не совпадает. Рядом указаны соответствующие разделы.
- Пересмотр прав администратора (3.2)
Сначала отказаться от постоянного
requireAdministrator. Радиус ущерба меняется сразу, а проектное изменение часто небольшое. - Подпись и метка времени (3.3) Навести доверие к дистрибутиву. Достаточно встроить в процедуру; если вставлять потом, понадобится повторная раздача.
- Убрать секреты (3.5) Вынести секреты из исходников и plaintext-конфига. Ущерб от утечки большой, после утечки уже не вернуть.
- Починить HTTPS и проверку сертификата (3.6)
Убрать из поставки конструкции
=> true. Часто лечится удалением; если оставить, всей сети нельзя верить. - Пересмотреть ввод SQL / файлов / IPC (3.7) Уменьшить конкатенацию и непроверенный ввод. Мест много, но чинится по одному.
- Зафиксировать загрузку DLL (3.8) Не грузить только по имени и не надеяться на PATH. Это правка раннего старта, плюс профилактика сбоев.
- Маскирование логов (3.9) Чтобы при инциденте лог не стал вторым ударом. Точек вывода много, времени уйдёт больше.
- Регулярное обновление зависимостей (3.10) Проверка на каждом релизе. Одним разом не закрывается: работа как раз в том, чтобы сделать это процессом.
Канала обновления (3.4) в этом списке нет, потому что автообновления нет у всех приложений. Если оно есть, смотрите его с тем же приоритетом, что пункт 2 про подпись. Модуль обновления слабее основного кода, но как раз он может этот код переписать.
flowchart TB
accTitle: Как расставлять приоритеты
accDescr: Порядок — произведение масштаба ущерба и дешевизны исправления, сначала самые опасные дыры; если есть автообновление, канал обновления смотрите с тем же приоритетом, что подпись.
dmg1["Масштаб ущерба"] --> mul1["Порядок = произведение двух осей"]
cst1["Дешевизна исправления"] --> mul1
mul1 --> ord1["Закрывать сначала самое опасное"]
ord1 -.-> upn1["Если есть автообновление, канал обновления — как подпись"]
Рис. 21: Приоритет — ущерб × дешевизна исправления; закрываем сначала самые опасные дыры.
В таком порядке проще двигаться в духе «сначала закрыть явно опасные дыры».
7. Итог
Безопасность разработки Windows-приложений заметно меняется ещё до специальных продуктов и больших систем, если привести в порядок семь пунктов: права, подпись, секреты, сеть, ввод, DLL, логи.
Минимальная планка по одной строке на пункт:
- Не запускать всё приложение от администратора
- Подписывать дистрибутив и обновления, ставить метку времени
- Не класть секреты в исходники и plaintext-конфиг
- Даже с HTTPS не отключать проверку сертификатов
- Не доверять внешнему вводу: SQL, файлы, IPC и остальное
- Не оставлять источник загрузки DLL размытым
- Не класть секреты в лог
- Не забрасывать библиотеки-зависимости
Тема безопасности широкая, и с самого начала делать всё не обязательно. Но минимум — не выпускать опасное поведение по умолчанию как есть — стоит закрыть довольно рано.
8. Источники
- Administrator Broker Model - Win32 apps
- How User Account Control works
- Authenticode Digital Signatures
- Time Stamping Authenticode Signatures
- Sign a Windows app package
- Credential Locker for Windows apps
- CryptProtectData function (dpapi.h)
- CA5359: Do not disable certificate validation
- CA5399: Enable HttpClient certificate revocation list check
- Configuring parameters - ADO.NET Provider for SQL Server
- Connection String Syntax - ADO.NET
- Dynamic-Link Library Security - Win32 apps
- SetDefaultDllDirectories function (libloaderapi.h)
- Data redaction in .NET
- ProtectedData Class - о том, что класс только для Windows, и замечание на случай, когда профиль не загружен.
- ServicePointManager.ServerCertificateValidationCallback - начиная с .NET 9 отображается на настройки SocketsHttpHandler.
- Команда dotnet list package - с
--vulnerableможно проверить известные уязвимости. - Get-AuthenticodeSignature
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как в Windows-приложении вынести только операции, которым нужны права администратора
Разбираем, как оставить UI Windows-приложения asInvoker и вынести операции с правами администратора в helper EXE: UAC, runas, именованные...
Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI
Чтобы не хранить сведения о подключении и API-токены в конфигурационных файлах Windows-приложений открытым текстом, разбираем подход DPAP...
Глубины ввода-вывода Windows (часть 6, финал) — фильтры и минифильтры: почему Procmon и антивирусное сканирование могут перехватывать I/O
Финал серии со схемами драйверов фильтров и минифильтров Windows. Диспетчер фильтров и высота (altitude), обратные вызовы pre/post, как P...
Безопасный вызов Win32 API из C# — практическое руководство по P/Invoke (DllImport / LibraryImport / CsWin32)
Разбираем, как на практике вызывать Win32 API из C# через P/Invoke: чем DllImport отличается от LibraryImport, как CsWin32 генерирует сиг...
Как работать с токенами олицетворения Windows — олицетворение на уровне потока и безопасный откат
В статье собраны практические правила безопасной работы с токенами олицетворения Windows: токен доступа, первичный токен, токен потока, у...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Речь о пересмотре Windows-приложения целиком — от модели прав и способа распространения до механизма обновления и логирования, — поэтому тема хорошо стыкуется с разработкой Windows-приложений.
Технические консультации и ревью дизайна
Если хотите начать с аудита безопасности существующего приложения, границ прав или политики апдейтера, это можно оформить как техническую консультацию и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что смотреть в первую очередь в безопасности Windows-приложения?
- Четыре пункта: не запрашивать лишние права администратора, подписывать код, не хранить секреты в открытом виде и не отключать проверку сертификатов. Минимальная безопасность — не про особые функции, а про то, чтобы не оставлять опасное поведение по умолчанию и небрежную реализацию. Даже на этой планке стоит избегать постоянного пропуска проверки сертификата, plaintext-строк подключения, загрузки DLL из текущего каталога и SQL через конкатенацию строк.
- Нельзя ли просто запускать всё приложение с правами администратора?
- Этого лучше не делать. Если всё приложение работает от администратора, баги, подмена DLL, неверное чтение конфигурации и дыры во внешнем вводе исполняются уже с этими высокими правами. Обычный UI-приложение держите на asInvoker, а операции, которым нужны права администратора, выносите в отдельный процесс или службу и повышайте права только в нужный момент.
- Где хранить API-ключи и строки подключения?
- Сначала уберите их из исходников и plaintext-конфигов вроде appsettings.json. Для секретов на диске в зависимости от задачи берите DPAPI / ProtectedData или Credential Locker. Если в лог уходят токены, пароли, строки подключения и персональные данные как есть, сам лог становится источником инцидента — такие поля нужно маскировать.
- Нужна ли подпись кода, если приложение распространяется только внутри компании?
- Да, это лучше считать обязательным. В Windows-приложениях поверхность атаки — сам дистрибутив (EXE / DLL / MSI / MSIX / модуль автообновления). Подпись кода плюс метка времени дают обнаружение подмены, доверие пользователей и понятное объяснение на эксплуатации. Канал обновления тоже должен фиксировать источник и ловить подмену через HTTPS и проверку подписи.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.