
Как укрепить старый сайт на ASP.NET Web Forms: ViewState MAC, SQL-инъекции, заголовки и TLS 1.2
Сайт на Web Forms, написанный пятнадцать лет назад, часто работает без единой правки кода и так же давно не проверялся на безопасность. Собрали меры, которые закрывают самые частые дыры и почти не требуют переписывания страниц.
ViewState: MAC и ключи machineKey
ViewState хранит состояние страницы в скрытом поле и возвращается на сервер при каждом постбэке. Если злоумышленник может подделать его содержимое, он получает десериализацию произвольных данных на сервере, а это прямой путь к выполнению кода. Защита от подделки это MAC, подпись ViewState ключом из раздела machineKey.
Начиная с обновления безопасности 2014 года для .NET Framework 4.5.2 и всех более новых версий отключить MAC нельзя: атрибут EnableViewStateMac="false" игнорируется. Поэтому главный риск сегодня не выключенная подпись, а скомпрометированный ключ. В феврале 2025 года Microsoft Threat Intelligence сообщила о реальных атаках с использованием ключей machineKey, опубликованных в открытых репозиториях и статьях: тысячи сайтов скопировали значения из примеров кода.
Проверьте web.config всех приложений. Если validationKey и decryptionKey заданы явно, убедитесь, что они сгенерированы для вашего сайта и не встречаются в поиске по интернету. Хранить их нужно вне репозитория, например в зашифрованной секции конфигурации. Если ключ хоть раз попадал в общий доступ, смените его: пользователи потеряют сессии и формы на текущих страницах, но это меньшее зло.
ViewStateUserKey против CSRF
Подпись защищает от подделки содержимого, но не от повторного использования чужого ViewState. Свойство ViewStateUserKey привязывает подпись к конкретному пользователю: страница, сгенерированная для одного сеанса, не примется в другом. Это закрывает межсайтовую подделку запросов для постбэков Web Forms.
Удобнее всего задать ключ в базовом классе страниц, от которого наследуются все остальные. Если в проекте такого класса нет, это хороший повод его завести.
Public Class BasePage
Inherits System.Web.UI.Page
Protected Overrides Sub OnInit(e As EventArgs)
MyBase.OnInit(e)
If User.Identity.IsAuthenticated Then
ViewStateUserKey = User.Identity.Name
ElseIf Context.Session IsNot Nothing Then
ViewStateUserKey = Session.SessionID
End If
End Sub
End ClassSQL-инъекции: склейка строк и SqlDataSource
В старых проектах запросы часто собираются конкатенацией: "SELECT ... WHERE Name = '" & txtName.Text & "'". Замена апострофа на два апострофа не спасает: остаются числовые параметры без кавычек, сортировка по имени колонки из строки запроса и динамические фильтры. Единственная надёжная защита это параметры.
Проверьте не только код в .vb-файлах, но и разметку. SqlDataSource с SelectCommand, собранным в обработчике Selecting через строку, так же уязвим. Правильный вариант: параметры в коллекции SelectParameters с ControlParameter или QueryStringParameter.
Public Function FindOrders(customer As String, sortColumn As String) As DataTable
' Имена колонок нельзя передать параметром, поэтому только белый список
Dim allowed As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) From {"OrderDate", "Total", "Number"}
If Not allowed.Contains(sortColumn) Then sortColumn = "OrderDate"
Dim sql = "SELECT Number, OrderDate, Total FROM Orders " &
"WHERE CustomerName = @customer ORDER BY " & sortColumn
Using cn As New SqlConnection(_connectionString),
cmd As New SqlCommand(sql, cn)
cmd.Parameters.Add("@customer", SqlDbType.NVarChar, 200).Value = customer
Dim table As New DataTable()
Using da As New SqlDataAdapter(cmd)
da.Fill(table)
End Using
Return table
End Using
End FunctionЗаголовки и cookie
Большую часть защитных заголовков можно добавить в web.config без правки страниц. Заодно стоит убрать заголовки, которые сообщают атакующему версию платформы.
- атрибут removeServerHeader поддерживается в IIS 10 начиная с Windows Server версии 1709 и Windows Server 2019, на старых версиях IIS он вызовет ошибку конфигурации;
- Content-Security-Policy для Web Forms начинайте с режима Content-Security-Policy-Report-Only: инлайновые скрипты __doPostBack и WebResource.axd сломаются при строгой политике;
- HSTS включайте только после того, как весь сайт и поддомены доступны по HTTPS.
<system.web>
<httpRuntime targetFramework="4.8" enableVersionHeader="false" />
<httpCookies httpOnlyCookies="true" requireSSL="true" sameSite="Lax" />
<customErrors mode="RemoteOnly" defaultRedirect="~/error.aspx" />
<compilation debug="false" targetFramework="4.8" />
<trace enabled="false" />
</system.web>
<system.webServer>
<security>
<requestFiltering removeServerHeader="true" />
</security>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<add name="Strict-Transport-Security" value="max-age=31536000" />
</customHeaders>
</httpProtocol>
</system.webServer>TLS 1.2 для исходящих соединений
Входящий HTTPS настраивает IIS и Windows, а вот исходящие вызовы приложения (платёжный шлюз, SOAP-сервис партнёра, SMTP) зависят от версии .NET Framework и от того, под какую версию собран сайт. Приложение с targetFramework 4.7 и новее использует протоколы, разрешённые операционной системой. Приложение, собранное под 4.5 или 4.6, может по умолчанию предлагать устаревшие протоколы, и современный сервер разорвёт соединение.
Правильное решение это перевести сайт на httpRuntime targetFramework 4.8 (после проверки поведения) или включить в реестре значения SystemDefaultTlsVersions и SchUseStrongCrypto для .NET Framework. Жёстко прописанный в коде ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 работает, но через пару лет помешает перейти на TLS 1.3, поэтому оставляйте его только как временную меру.
Остальное из чек-листа
Пройдитесь по этому списку для каждого приложения на сервере, а не только для основного сайта: забытая админка на соседнем пуле с тем же machineKey ломает защиту всех остальных.
- ошибки: customErrors в RemoteOnly или On, детали исключений только в журнал;
- debug="false" и выключенный trace.axd на продакшене;
- проверка запросов: не отключайте validateRequest глобально, а кодируйте вывод через Server.HtmlEncode и <%: %>;
- загрузка файлов: проверка расширения по белому списку, сохранение вне каталога сайта, запрет исполнения в папке загрузок;
- пул приложений под отдельной учётной записью с минимальными правами на файлы и базу;
- актуальные обновления .NET Framework 4.8 и Windows Server: многие уязвимости закрываются только патчами платформы.
Как это делают сегодня
Вопросы и ответы
Нужно ли шифровать ViewState?
Шифрование скрывает содержимое, но не заменяет подпись. Включайте ViewStateEncryptionMode, если в ViewState попадают данные, которые пользователь видеть не должен. Лучше такие данные туда вообще не класть.
Как понять, что machineKey скомпрометирован?
Поищите значение validationKey в интернете и в публичных репозиториях. Если ключ взят из статьи или пример конфигурации попадал в общий доступ, считайте его скомпрометированным и смените.
Достаточно ли включить TLS 1.2 в реестре Windows?
Для входящих соединений IIS обычно да. Для исходящих вызовов приложения важна ещё версия .NET Framework, под которую оно собрано, и ключи SystemDefaultTlsVersions и SchUseStrongCrypto.