Как укрепить старый сайт на ASP.NET Web Forms: ViewState MAC, SQL-инъекции, заголовки и TLS 1.2

РубрикаБезопасностьУровеньОпытный Чтение10 минОпубликовано ТегиASP.NET Web FormsБезопасностьweb.configTLS

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

Главное. Для старого Web Forms важнее всего уникальный и закрытый machineKey, ViewStateUserKey, параметризованные запросы, защитные заголовки в web.config и targetFramework 4.7+ для TLS 1.2. Всё это делается без переписывания страниц.

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.

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

BasePage.vbVB.NET
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 Class

SQL-инъекции: склейка строк и SqlDataSource

В старых проектах запросы часто собираются конкатенацией: "SELECT ... WHERE Name = '" & txtName.Text & "'". Замена апострофа на два апострофа не спасает: остаются числовые параметры без кавычек, сортировка по имени колонки из строки запроса и динамические фильтры. Единственная надёжная защита это параметры.

Проверьте не только код в .vb-файлах, но и разметку. SqlDataSource с SelectCommand, собранным в обработчике Selecting через строку, так же уязвим. Правильный вариант: параметры в коллекции SelectParameters с ControlParameter или QueryStringParameter.

OrdersRepository.vbVB.NET
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
Используйте Parameters.Add с явным типом и длиной, а не AddWithValue. AddWithValue выводит тип из значения .NET, и строка превращается в nvarchar нужной длины, из-за чего SQL Server может не использовать индекс по varchar-колонке.

Заголовки и 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.
web.config (фрагмент)XML
<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: многие уязвимости закрываются только патчами платформы.

Как это делают сегодня

В ASP.NET Core механизмы ViewState и machineKey заменены Data Protection API, антифорджери-токены встроены в формы, а заголовки и HSTS настраиваются в конвейере middleware. Если сайт всё равно переезжает, эти вопросы решаются платформой по умолчанию.

Вопросы и ответы

Нужно ли шифровать ViewState?

Шифрование скрывает содержимое, но не заменяет подпись. Включайте ViewStateEncryptionMode, если в ViewState попадают данные, которые пользователь видеть не должен. Лучше такие данные туда вообще не класть.

Как понять, что machineKey скомпрометирован?

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

Достаточно ли включить TLS 1.2 в реестре Windows?

Для входящих соединений IIS обычно да. Для исходящих вызовов приложения важна ещё версия .NET Framework, под которую оно собрано, и ключи SystemDefaultTlsVersions и SchUseStrongCrypto.

Читайте также