Защита параметров странички от подделки
На этой страничке я расскажу, как защитить свою страничку от взлома. Таких методов много. Самый лучший из них - написание своего HttpModule, который будет делать URL Rewriting котором все ссылки проекта станут просто бессмысленными - типа http://MyDomain/GUID.
Судя по всему - это замечательный способ, им пользуются многие, но мне не совсем понятно пока как он будет работать с клиентскими скриптами и я пока не решился применить его на практике. Зато я очень люблю всякие криптографические фишки и я написал вот такой класс для защиты параметров страничек от подделок. Все критические номера в ссылках сайта выглядят при этом вот так:
http://SH.RU/MyPage.aspx?id1=2140F86D35B6F403C2C26848ACA5521F&id2=4017564F3C13ABFA4EBD260F57067392
В данном случае две ссылки, приведенные выше, скрывают всего-навсего символы 1 и 2, которые мне бы очень не хотелось показывать юзеру в открытом виде http://SH.RU/MyPage.aspx?id1=1&id2=2 и уж тем более не хотелось бы, чтоб он их подменил. Номера эти всегда получаются разные и при малейшей попытке подделать параметры странички, хитрец сразу же редиректится на страничку с предупреждением или логином.
Защита инициализируется четырьмя строчками в GLOBAL.ASAX:
Application("ParmProtector8") = New ParmProtector8 CType(Application("ParmProtector8"), ParmProtector8).RedirectUrl = "AccessDenied.aspx" CType(Application("ParmProtector8"), ParmProtector8).ConfigKey = "CryptURLParm" CType(Application("ParmProtector8"), ParmProtector8).SpecialChar = "*"и в любой момент может быть выключена из WEB-Config и
<add key="CryptURLParm" value="True" />Собственно защита параметра происходит при вызове метода Mask:
Dim id1 As String="1", id2 As String="2" Dim Tag As String = "<a href='MyPage.aspx?id1=" & _ CType(Application("ParmProtector8"), ParmProtector8).Mask(id1) & _ "&id2=" & CType(Application("ParmProtector8"), ParmProtector8).Mask(id2) & "'>.......</a>"Соответственно восстановление параметра происходит при вызове метода UnMask:
id1 = CType(Application("ParmProtector8"), ParmProtector8).UnMask(Request.QueryString("id1"))
Последний параметр в инициализации - SpecialChar, является уникальным модификатором алгоритма, позволяющим внести в него индивидуальность. На тот случай, если взломщик даже точно знает, что для защиты ключей использован этот мой модуль и взломщик УЖЕ имеет полный текущий ASP2-код этой защиты, минимум, что ему придется сделать для подбора защищенного ключа - это написать свое собственное приложение, имитирующую мой алгоритм, но при этом ему все равно взламывать сайт придется только путем полного перебора модификаторов. Однако для критических приложений важно зафиксировать факт атаки. И что мешает произвести более жесткую реацию на попытку взлома после нескольких подряд попаданий на страничку AccessDenied?
В принципе эту схему защиты легко расширить на произвольную длину защищаемых параметров (для этого надо просто увеличить длину буфера при расшифровке), но во-первых, длина URL ограничена, во-вторых, это производительность, а в-третьих, это назначение. Эта система в принципе предназначена для маскирования номеров записей в базе (например чтобы юзер не получил чью-то чужую запись), а количество записей вряд-ли будет более 100 миллионов. Поэтому я ограничил длину защищаемого параметра в восемь символов.
Кроме того, как вы видите, в строке 58 я уложил в созданный криптографический идентификатор и SessionID, однако не использовал его в строке 80 при проверке подлинности полученного идентификатора. Это сделать можно, но такое ужесточение этого алгоритма может привести к проблемам, когда одна форма порождает множество ASP2-форм. Можно ввести параметр "более или менее жесткая" проверка подлинности полученного криптографического идентификатора. Впрочем расширять это алгоритм возможно в любую сторону до бесконечности.
Вот на этом варианте я остановился в итоге. Он достаточно трудно взламывается, обладает хорошей универсальностью и хорошо расширяется в любую сторону.
00001: Public Class ParmProtector8 00002: Dim DES As New System.Security.Cryptography.DESCryptoServiceProvider 00003: Dim _Delimiter As Char = "*" 00004: Dim _RedirectURL As String 00005: Dim _ConfigKey As String 00006: 00007: Public Sub New() 00008: DES.GenerateKey() 00009: End Sub 00010: 00011: ''' <summary> 00012: ''' Модификатор алгоритма (один спецсимвол) 00013: ''' </summary> 00014: Public Property SpecialChar() As char 00015: Get 00016: SpecialChar = _Delimiter 00017: End Get 00018: Set(ByVal value As char) 00019: _Delimiter = value 00020: End Set 00021: End Property 00022: 00023: ''' <summary> 00024: ''' URL станички для редиректа при попытке взлома параметра 00025: ''' </summary> 00026: Public Property RedirectUrl() As String 00027: Get 00028: RedirectUrl = _RedirectURL 00029: End Get 00030: Set(ByVal value As String) 00031: _RedirectURL = value 00032: End Set 00033: End Property 00034: 00035: ''' <summary> 00036: ''' имя параметра конфига TRUE/FALSE - надо ли шифровать 00037: ''' </summary> 00038: Public Property ConfigKey() As String 00039: Get 00040: ConfigKey = _ConfigKey 00041: End Get 00042: Set(ByVal value As String) 00043: _ConfigKey = value 00044: End Set 00045: End Property 00046: 00047: ''' <summary> 00048: ''' Параметр для шифрования - не более восьми символов длиной () 00049: ''' </summary> 00050: Public Function Mask(ByVal RawParm As String) As String 00051: If Not CBool(System.Configuration.ConfigurationManager.AppSettings(_ConfigKey)) Then Return RawParm 00052: If RawParm.Length > 8 Then Throw New Exception("Шифрование параметров длины более 8 символов не поддерживается." & vbCrLf & RawParm) 00053: Dim [In] As New System.Text.StringBuilder 00054: [In].Append(Now.Ticks.ToString.Substring(10, 2).Replace("0", "#")) 00055: [In].Append(_Delimiter) 00056: [In].Append(RawParm) 00057: [In].Append(_Delimiter) 00058: [In].Append(System.Web.HttpContext.Current.Session.SessionID.ToString) 00059: [In].Length = 12 'эта длина соотвествует буферу для расшифровки в 16 байт. 00060: Dim CryptBytes As Byte() = Encrypt([In].ToString) 00061: Dim [Out] As New System.Text.StringBuilder 00062: For Each One As Byte In CryptBytes 00063: [Out].Append(One.ToString("X2")) 00064: Next 00065: Return [Out].ToString 00066: End Function 00067: 00068: ''' <summary> 00069: ''' Расшифровка параметра с редиректом на страницу ошибок. 00070: ''' </summary> 00071: Public Function UnMask(ByVal CryptParm As String) As String 00072: If Not CBool(System.Configuration.ConfigurationManager.AppSettings(_ConfigKey)) Then Return CryptParm 00073: If CryptParm.Length <> 32 Then System.Web.HttpContext.Current.Response.Redirect(_RedirectURL) 00074: Dim Buf(15) As Byte 'буфер для расшифровки 00075: For Z As Integer = 1 To CryptParm.Length - 1 Step 2 00076: Dim b As Byte = Byte.Parse(Mid(CryptParm, Z, 2), Globalization.NumberStyles.HexNumber) 00077: Buf((Z - 1) / 2) = b 00078: Next 00079: Dim Out As String = Decrypt(Buf) 00080: If Out.IndexOf(_Delimiter) = 2 Then 00081: Dim EndParmPos As Integer = Out.IndexOf(_Delimiter, 3) 00082: If EndParmPos > 0 Then 00083: Return Out.Substring(3, EndParmPos - 3) 00084: Else 00085: System.Web.HttpContext.Current.Response.Redirect(_RedirectURL) 00086: End If 00087: Else 00088: System.Web.HttpContext.Current.Response.Redirect(_RedirectURL) 00089: End If 00090: End Function 00091: 00092: 'Encrypt the string to array of bytes 00093: Private Function Encrypt(ByVal PlainText As String) As Byte() 00094: Dim ms As New System.IO.MemoryStream() 00095: Dim encStream As New System.Security.Cryptography.CryptoStream(ms, DES.CreateEncryptor(), System.Security.Cryptography.CryptoStreamMode.Write) 00096: Dim sw As New System.IO.StreamWriter(encStream) 00097: sw.WriteLine(PlainText) 00098: sw.Close() 00099: encStream.Close() 00100: Dim buffer As Byte() = ms.ToArray() 00101: ms.Close() 00102: Return buffer 00103: End Function 00104: 00105: 00106: 'Decrypt the byte array to string 00107: Private Function Decrypt(ByVal CypherText() As Byte) As String 00108: Dim ms As New System.IO.MemoryStream(CypherText) 00109: Dim encStream As New System.Security.Cryptography.CryptoStream(ms, DES.CreateDecryptor(), System.Security.Cryptography.CryptoStreamMode.Read) 00110: Dim sr As New System.IO.StreamReader(encStream) 00111: Dim val As String 00112: Try 00113: val = sr.ReadLine() 00114: Catch ex As System.Security.Cryptography.CryptographicException 00115: System.Web.HttpContext.Current.Response.Redirect(_RedirectURL) 00116: End Try 00117: sr.Close() 00118: encStream.Close() 00119: ms.Close() 00120: Return val 00121: End Function 00122: 00123: End Class
Сгрузить этот класс в готовом виде и сразу же использовать его в своих проектах - вы можете отсюда.
Просуществовав в таком виде два года, впоследствии я подверг этот код нижеследующей доработке. Которая нужна в том случае, когда странички с защифрованными линками УКЛАДЫВАЮТСЯ В БАЗУ.
Естественно при рестарте приложения достать странички из базы и расшифровать их - будет невозможно. Поэтому класс пришлось дооснастить вот такими методами:
00001: ''' <summary> 00002: ''' При рестарте приложения ключи будут новые и все старое расшифровать не удастся 00003: ''' Этот метод позволяет сохранить ключи в базу. Методу надо отдать имя строки коннекта к базе 00004: ''' CREATE TABLE [dbo].[ParmProtector8]( 00005: ''' [i] [int] IDENTITY(1,1) NOT NULL, 00006: ''' [key] [binary](8) NOT NULL, 00007: ''' [IV] [binary](8) NOT NULL, 00008: '''CONSTRAINT [PK_ParmProtector8] PRIMARY KEY CLUSTERED 00009: ''' ( [i] ASC ) ON [PRIMARY] 00010: ''' ) ON [PRIMARY] 00011: ''' </summary> 00012: Public Function SaveKeyToSQL(ByVal ConnectionStringName As String) As Integer 00013: Dim CN As New System.Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings(ConnectionStringName).ConnectionString) 00014: Dim CMD As New System.Data.SqlClient.SqlCommand("INSERT [ParmProtector8]([key],[IV],[Date]) VALUES (@Key,@IV, GetDate()); select scope_identity() as [i]") 00015: CMD.CommandType = Data.CommandType.Text 00016: CMD.Parameters.Add("Key", Data.SqlDbType.Binary, 8) 00017: CMD.Parameters.Add("IV", Data.SqlDbType.Binary, 8) 00018: CN.Open() 00019: CMD.Connection = CN 00020: CMD.Parameters("Key").Value = DES.Key 00021: CMD.Parameters("IV").Value = DES.IV 00022: Dim RDR = CMD.ExecuteReader(System.Data.CommandBehavior.SingleRow) 00023: If RDR.Read Then 00024: Restart_DB_ID = RDR("I") 00025: End If 00026: RDR.Close() 00027: CN.Close() 00028: CMD = Nothing 00029: End Function 00030: 00031: ''' <summary> 00032: ''' Метод инициализирует класс шифрования. Принимает на вход номер записи в базе с ключами и имя строки коннекта к базе 00033: ''' </summary> 00034: Public Sub LoadKeyFromSQL(ByVal ConnectionStringName As String, ByVal DB_ID As Integer) 00035: Restart_DB_ID = DB_ID 00036: Dim CN As New System.Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings(ConnectionStringName).ConnectionString) 00037: Dim CMD As New System.Data.SqlClient.SqlCommand("SELECT [key],[IV] From [ParmProtector8] where i=" & DB_ID.ToString) 00038: CMD.CommandType = Data.CommandType.Text 00039: CN.Open() 00040: CMD.Connection = CN 00041: Dim RDR = CMD.ExecuteReader(System.Data.CommandBehavior.SingleRow) 00042: If RDR.Read Then 00043: DES.Key = RDR("key") 00044: DES.IV = RDR("IV") 00045: End If 00046: RDR.Close() 00047: CN.Close() 00048: CMD = Nothing 00049: End Sub 00050: 00051: Private Restart_DB_ID 'номер записи в базе, куда сохранен ключ и IV симметричного шифрования (защита от рестарта) 00052: Public ReadOnly Property DB_ID() As Integer 00053: Get 00054: Return Restart_DB_ID 00055: End Get 00056: End Property
И номер сеансового ключа в базе ДОБАВЛЯТЬ к параметру. Это, кстати, позволит корректно обрабатывать запросы из референсов, сохраненных в клиентских браузерах.
Теперь, соответственно изменится и процедура инициализации этого класса - создавая каждый раз новый "сеансовый" ключ (до следующего авторестарта приложения) и инициализируя им наш класс шифрования:
Для фишинговых сайтов, которые выполняют криптовку параметров прямо в хандлере, придется заменить также строку 58 в первом фрагменте текста - ибо, как вы понимаете - в хандлере сессиона нету. В заполнитель подойдет фрагмент гуида, полученный на строчку выше.
Для работе этот класс надо ЛОЧИТЬ, ибо для экономии ресурсов я сделал его статическим - фактически ОДИН экземпляр его работает во всех приложениях. Возможно, это не очень удачная затея с точки зрения масштабируемости - но тем не менее, если использовать именно такую технологию криптовки параметров, как я описал на этой страничке - корректно мой класс будет работать - только будучи защищенным от мультипоточности скобками SyncLock:
Этот класс можно переделать из статического - если производительность этого моего весьма экономного решения будет сдерживать работу вашего приложения. Дальнейший путь совершенствования этого класса - докручивание в нем асинхронности. И если первая задача является чисто технической - вторая (в хандлере!) - представляет собой настоящую игру извращенного ума!
Желаю удачи!
|