Избегайте SQL-инъекции без параметров


109

У нас здесь еще одна дискуссия об использовании параметризованных запросов sql в нашем коде. У нас есть две стороны в обсуждении: я и некоторые другие, которые говорят, что мы всегда должны использовать параметры для защиты от инъекций sql, и другие ребята, которые не думают, что это необходимо. Вместо этого они хотят заменить отдельные апострофы двумя апострофами во всех строках, чтобы избежать инъекций sql. Все наши базы данных работают под управлением Sql Server 2005 или 2008, а наша кодовая база работает на .NET framework 2.0.

Приведу простой пример на C #:

Я хочу, чтобы мы использовали это:

string sql = "SELECT * FROM Users WHERE Name=@name";
SqlCommand getUser = new SqlCommand(sql, connection);
getUser.Parameters.AddWithValue("@name", userName);
//... blabla - do something here, this is safe

Пока другие ребята хотят это сделать:

string sql = "SELECT * FROM Users WHERE Name=" + SafeDBString(name);
SqlCommand getUser = new SqlCommand(sql, connection);
//... blabla - are we safe now?

Где функция SafeDBString определяется следующим образом:

string SafeDBString(string inputValue) 
{
    return "'" + inputValue.Replace("'", "''") + "'";
}

Теперь, пока мы используем SafeDBString для всех строковых значений в наших запросах, мы должны быть в безопасности. Правильно?

Есть две причины использовать функцию SafeDBString. Во-первых, так это делалось с каменных веков, а во-вторых, проще отлаживать операторы sql, поскольку вы видите точный запрос, выполняемый в базе данных.

Итак. Мой вопрос в том, действительно ли достаточно использовать функцию SafeDBString, чтобы избежать атак с использованием sql-инъекций. Я пытался найти примеры кода, который нарушает эту меру безопасности, но не могу найти никаких примеров.

Есть ли кто-нибудь, кто может это сломать? Как бы ты это сделал?

РЕДАКТИРОВАТЬ: Подводя итоги ответов на данный момент:

  • Еще никто не нашел способа обойти SafeDBString на Sql Server 2005 или 2008. Думаю, это хорошо?
  • В нескольких ответах отмечалось, что вы получаете прирост производительности при использовании параметризованных запросов. Причина в том, что планы запроса можно использовать повторно.
  • Мы также согласны с тем, что использование параметризованных запросов дает более читаемый код, который легче поддерживать.
  • Кроме того, проще всегда использовать параметры, чем использовать различные версии SafeDBString, преобразования строки в числа и преобразования строки в дату.
  • Используя параметры, вы получаете автоматическое преобразование типов, что особенно полезно, когда мы работаем с датами или десятичными числами.
  • И наконец: не пытайтесь самостоятельно обеспечивать безопасность, как писал JulianR. Поставщики баз данных тратят много времени и денег на безопасность. Нет способа сделать что-то лучше и нет причин, по которым мы должны пытаться делать их работу.

Так что, хотя никто не смог взломать простую безопасность функции SafeDBString, у меня есть много других хороших аргументов. Спасибо!


16
Ваши коллеги далеко-далеко, не на базе. Попросите их найти хоть один отрывок в поддержку их позиции. Аргумент ex neolithos смехотворен, все меняется, только человек, застрявший в каменном веке, не сможет адаптироваться.
annakata

1
Что ж, по крайней мере, ваши коллеги защищают от ОДНОЙ из различных форм взлома ... Они уверены, что все параметризованные запросы подходят? (Я не ...)
Арджан Эйнбу

1
Ни одна уязвимость их не убедит. Если вы принесете несколько уязвимостей (это то, о чем вы просите) и другие проблемы и укажете один за другим, что параметры решат эту проблему и что вашей команде придется написать горы кода, чтобы обеспечить часть функциональности, вы можете победить их. Удачи.
Роберт Гоуленд,

3
Даже без одинарных кавычек вы все равно можете взломать код с помощью логики. Попробуйте использовать имя пользователя «test OR 1 = 1» - вы получите все строки, а не только строку с именем пользователя test!
Бридж

1
Вздох. Я действительно не понимаю, как мы, как отрасль, можем продолжать терпеть такое непрофессиональное поведение.
jeroenh

Ответы:


83

Думаю, правильный ответ:

Не пытайтесь обеспечить безопасность самостоятельно . Используйте любую доступную надежную отраслевую стандартную библиотеку для того, что вы пытаетесь сделать, вместо того, чтобы пытаться сделать это самостоятельно. Какие бы предположения вы ни делали о безопасности, они могут быть неверными. Каким бы безопасным ни казался ваш собственный подход (и в лучшем случае он выглядит шатким), есть риск, что вы что-то упускаете, и действительно ли вы хотите воспользоваться этим шансом, когда дело касается безопасности?

Используйте параметры.


Re "Используйте любую надежную стандартную отраслевую библиотеку" - можете ли вы порекомендовать одну для .NET? Может быть, больше одного в зависимости от БД: SQLServer, MySQL, PostgreSQL? Я искал средство дезинфекции SQL, но без особого успеха, поэтому я был вынужден реализовать мое собственное, насколько я мог (что, несомненно, далеко не надежно).
PSU

72

И тогда кто-то идет и использует "вместо". Параметры, ИМО, единственный безопасный путь.

Это также позволяет избежать множества проблем с датами / числами; какая дата 01/02/03? Сколько будет 123456? Согласованы ли ваши серверы (app-server и db-server) друг с другом?

Если фактор риска их не убеждает, как насчет производительности? РСУБД может повторно использовать план запроса, если вы используете параметры, что способствует повышению производительности. Этого нельзя сделать только со строкой.


Я пробовал аргументы в пользу форматирования и производительности, но они все еще не убедили.
Rune Grimstad

5
Фактически, sql server может повторно использовать план запроса независимо от того, используете вы параметры или нет. Я согласен с другими аргументами, но в большинстве случаев аргумент производительности для параметризованного sql больше не работает.
tnyfst

1
@tnyfst: он может повторно использовать план выполнения, когда строка запроса изменяется для каждой комбинации значений параметров? Я не думал, что это возможно.
Джон Сондерс,

4
План запроса будет повторно использован, если текст запроса ИДЕНТИЧЕН предыдущему тексту запроса. Поэтому, если вы дважды отправите ТОЧНЫЙ ЖЕСТКИЙ запрос, он будет повторно использован. Однако, если вы измените хотя бы пробел, запятую или что-то еще, придется определить новый план запроса.
marc_s

1
@Marc: Я не уверен, что вы полностью правы. Херистика кеширования SQL-серверов немного странная. Анализатор способен определять константы в тексте и может искусственно преобразовывать строку SQL в параметры использования. Затем он может вставить в кэш текст этого нового параметризованного запроса. Последующий аналогичный SQL может найти свою параметризованную версию в кэше. Однако параметризованные версии не всегда используются с кэшируемыми исходными версиями SQL; я подозреваю, что у SQL есть масса причин, связанных с производительностью, чтобы выбирать между двумя подходами.
AnthonyWJones

27

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

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

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

Я предполагаю, что на самом деле проблема заключается в гордости и упрямстве, и с этим ничего не поделаешь.


19

Во-первых, ваш образец для версии «Заменить» неверен. Вам нужно поставить апострофы вокруг текста:

string sql = "SELECT * FROM Users WHERE Name='" + SafeDBString(name) & "'";
SqlCommand getUser = new SqlCommand(sql, connection);

Параметры делают для вас еще одно: вам не нужно беспокоиться о том, нужно ли заключать значение в кавычки. Конечно, вы можете встроить это в функцию, но тогда вам нужно добавить к функции много сложностей: как узнать разницу между NULL как null и NULL как просто строкой или между числом и строка, которая просто содержит много цифр. Это просто еще один источник ошибок.

Другое дело - производительность: параметризованные планы запросов часто кэшируются лучше, чем составные планы, что, возможно, экономит серверу шаг при выполнении запроса.

Кроме того, экранирования одинарных кавычек недостаточно. Многие продукты БД позволяют использовать альтернативные методы экранирования символов, которыми может воспользоваться злоумышленник. В MySQL, например, вы также можете экранировать одинарную кавычку с помощью обратной косой черты. Таким образом, следующее значение «name» взорвет MySQL только с помощью SafeDBString()функции, потому что, когда вы удваиваете одинарную кавычку, первая все еще экранируется обратной косой чертой, а вторая остается «активной»:

х \ 'ИЛИ 1 = 1; -


Кроме того, JulianR поднимает хороший момент ниже: НИКОГДА не пытайтесь самостоятельно выполнять работу по обеспечению безопасности. Очень легко ошибиться в программировании системы безопасности незаметными способами, которые кажутся работающими даже после тщательного тестирования. Затем проходит время, и год спустя вы обнаруживаете, что ваша система была взломана шесть месяцев назад, и вы даже не знали об этом до тех пор.

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


5
Функция замены добавляет апострофы
Rune Grimstad

5
Тогда это еще один источник ошибок. Как узнать разницу между NULL как нулевым значением и NULL как текстовой строкой? Или между вводом числа и строкой, которая просто содержит цифры?
Джоэл Кохорн,

Хорошая точка зрения. Вы должны использовать эту функцию только для строк и, возможно, дат, поэтому будьте осторожны. Это еще одна причина использовать параметры! Ура!
Руне Гримстад,

10

Я бы сказал:

1) Почему вы пытаетесь заново реализовать что-то встроенное? он там, легко доступен, прост в использовании и уже отлажен в глобальном масштабе. Если в будущем в нем будут обнаружены ошибки, они будут исправлены и доступны всем очень быстро, и вам не придется ничего делать.

2) Какие существуют процессы, гарантирующие, что вы никогда не пропустите вызов SafeDBString? Отсутствие его всего в одном месте может вызвать целый ряд проблем. Сколько вы собираетесь смотреть на эти вещи и учитывать, сколько зря потрачено на эти усилия, когда принятый правильный ответ так легко достичь.

3) Насколько вы уверены, что прикрыли каждый вектор атаки, о котором знает Microsoft (автор БД и библиотеки доступа), в вашей реализации SafeDBString ...

4) Насколько легко читать структуру sql? В примере используется + конкатенация, параметры очень похожи на string.Format, что более читабельно.

Кроме того, есть два способа выяснить, что на самом деле было запущено: развернуть собственную функцию LogCommand, простую функцию, не имеющую проблем с безопасностью , или даже посмотреть трассировку sql, чтобы выяснить, что, по мнению базы данных, действительно происходит.

Наша функция LogCommand проста:

    string LogCommand(SqlCommand cmd)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(cmd.CommandText);
        foreach (SqlParameter param in cmd.Parameters)
        {
            sb.Append(param.ToString());
            sb.Append(" = \"");
            sb.Append(param.Value.ToString());
            sb.AppendLine("\"");
        }
        return sb.ToString();
    }

Правильно это или нет, но он дает нам необходимую информацию без проблем с безопасностью.


1
Ему, вероятно, придется иметь дело с кучкой старых программистов VBSCRIPT, которые привыкли делать все, включая XML и SQL, путем конкатенации строк. Это будут люди, которых пугает использование API. С ними ничего нельзя сделать, по крайней мере, ничего гуманного.
Джон Сондерс

1
+1 для пункта 2, за исключением того, что нет возможности принудительно установить реальные параметры.
Джоэл Кохорн,

7

С помощью параметризованных запросов вы получаете больше, чем просто защиту от внедрения sql. Вы также получаете лучший потенциал кэширования плана выполнения. Если вы используете профилировщик запросов sql server, вы все равно можете видеть «точный sql, который запущен в базе данных», так что вы на самом деле ничего не теряете с точки зрения отладки ваших операторов sql.


MySQL также регистрирует параметризованные запросы со значениями параметров, вставленными в них.
Билл Карвин,

5

Я использовал оба подхода, чтобы избежать атак SQL-инъекций, и определенно предпочитаю параметризованные запросы. Когда я использовал составные запросы, я использовал библиотечную функцию, чтобы избежать переменных (например, mysql_real_escape_string), и не был бы уверен, что я охватил все в собственной реализации (как кажется, вы тоже).


2
+1, потому что mysql_real_escape_string () экранирует \ x00, \ x1a, \ n \ r 'и ". Он также обрабатывает проблемы с набором символов. Наивная функция коллег OP не делает ничего из этого!
Билл Карвин

4

Вы не можете легко выполнить какую-либо проверку типа вводимых пользователем данных без использования параметров.

Если вы используете классы SQLCommand и SQLParameter для выполнения вызовов DB, ​​вы все равно можете видеть выполняемый SQL-запрос. Посмотрите на свойство CommandText SQLCommand.

Я всегда мало подозреваю в применении индивидуального подхода к предотвращению внедрения SQL, когда параметризованные запросы настолько просты в использовании. Во-вторых, просто потому, что «так всегда делалось» не означает, что это правильный способ.


3

Это безопасно только в том случае, если вам гарантировано, что вы собираетесь передать строку.

Что, если в какой-то момент вы не передаете строку? Что, если вы передадите только число?

http://www.mywebsite.com/profile/?id=7;DROP DATABASE DB

В конечном итоге станет:

SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB

Это либо строка, либо число. Строка экранируется SafeDbString. Число - это Int32, и оно не может отбрасывать базы данных.
Andomar

Числа проще обрабатывать. Вы просто преобразуете параметр в int / float / something перед использованием его в запросе. Проблема в том, что вы должны принимать строковые данные.
Руне Гримстад,

Andomar - если вы просто создаете оператор SQL вручную, его предполагаемый «тип» не имеет значения, вы можете очень и очень легко вводить SQL с числом. Rune - я думаю, что это слишком много для отдельного разработчика, чтобы помнить все нюансы ручного решения SQL-инъекции. Если вы просто скажете «используйте параметры», это очень просто, и они не ошибутся.
joshcomley

@Andomar: А что насчет NULL? Или строки, похожие на числа?
Джоэл Кохорн,

2

Я бы использовал хранимые процедуры или функции для всего, поэтому вопрос не возникнет.

Там, где мне нужно вставить SQL в код, я использую параметры, и это единственное, что имеет смысл. Напомните несогласным, что есть хакеры, которые умнее их и имеют больше стимулов для взлома кода, который пытается их перехитрить. Используя параметры, это просто невозможно, да и не сложно.


Хорошо, как выполнить SQL-инъекцию с использованием параметров?
Джон Сондерс,

@Saunders: Шаг 1 - найти ошибку переполнения буфера в функциях обработки параметров вашей БД.
Брайан

2
Нашли еще? В коммерческой БД, которую ежедневно атакуют сотни тысяч хакеров? Один, сделанный компанией-разработчиком программного обеспечения с очень глубокими карманами? Вы смогли бы процитировать иск по имени , если это было возможно.
Джон Сондерс,

1
Конечно, если SPROC использует конкатенацию и EXEC (вместо sp_ExecuteSQL), у вас снова проблемы ... (Я слишком много раз видел, как это делалось неправильно, чтобы не учитывать ...)
Марк Грейвелл

2

Совершенно согласен по вопросам безопасности.
Еще одна причина использовать параметры - эффективность.

Базы данных всегда будут компилировать ваш запрос и кэшировать его, а затем повторно использовать кешированный запрос (что, очевидно, быстрее для последующих запросов). Если вы используете параметры, то даже если вы используете другие параметры, база данных будет повторно использовать ваш кешированный запрос, поскольку он соответствует на основе строки SQL перед привязкой параметров.

Однако, если вы не привязываете параметры, строка SQL изменяется при каждом запросе (с разными параметрами), и она никогда не будет соответствовать тому, что находится в вашем кеше.


2

По уже указанным причинам параметры - очень хорошая идея. Но мы ненавидим их использовать, потому что создание параметра и присвоение его имени переменной для последующего использования в запросе - это крушение тройного косвенного обращения.

Следующий класс является оболочкой для построителя строк, который вы обычно будете использовать для создания запросов SQL. Он позволяет вам писать параметризованные запросы без необходимости создавать параметр , поэтому вы можете сосредоточиться на SQL. Ваш код будет выглядеть так ...

var bldr = new SqlBuilder( myCommand );
bldr.Append("SELECT * FROM CUSTOMERS WHERE ID = ").Value(myId, SqlDbType.Int);
//or
bldr.Append("SELECT * FROM CUSTOMERS WHERE NAME LIKE ").FuzzyValue(myName, SqlDbType.NVarChar);
myCommand.CommandText = bldr.ToString();

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

Класс выглядит так ...

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace myNamespace
{
    /// <summary>
    /// Pour le confort et le bonheur, cette classe remplace StringBuilder pour la construction
    /// des requêtes SQL, avec l'avantage qu'elle gère la création des paramètres via la méthode
    /// Value().
    /// </summary>
    public class SqlBuilder
    {
        private StringBuilder _rq;
        private SqlCommand _cmd;
        private int _seq;
        public SqlBuilder(SqlCommand cmd)
        {
            _rq = new StringBuilder();
            _cmd = cmd;
            _seq = 0;
        }
        //Les autres surcharges de StringBuilder peuvent être implémenté ici de la même façon, au besoin.
        public SqlBuilder Append(String str)
        {
            _rq.Append(str);
            return this;
        }
        /// <summary>
        /// Ajoute une valeur runtime à la requête, via un paramètre.
        /// </summary>
        /// <param name="value">La valeur à renseigner dans la requête</param>
        /// <param name="type">Le DBType à utiliser pour la création du paramètre. Se référer au type de la colonne cible.</param>
        public SqlBuilder Value(Object value, SqlDbType type)
        {
            //get param name
            string paramName = "@SqlBuilderParam" + _seq++;
            //append condition to query
            _rq.Append(paramName);
            _cmd.Parameters.Add(paramName, type).Value = value;
            return this;
        }
        public SqlBuilder FuzzyValue(Object value, SqlDbType type)
        {
            //get param name
            string paramName = "@SqlBuilderParam" + _seq++;
            //append condition to query
            _rq.Append("'%' + " + paramName + " + '%'");
            _cmd.Parameters.Add(paramName, type).Value = value;
            return this; 
        }

        public override string ToString()
        {
            return _rq.ToString();
        }
    }
}

1

За очень короткое время, когда мне пришлось исследовать проблемы с SQL-инъекциями, я понял, что создание значения «безопасное» также означает, что вы закрываете дверь в ситуации, когда вам действительно могут понадобиться апострофы в своих данных - как насчет чьего-либо имени , например, О'Рейли.

Остались параметры и хранимые процедуры.

И да, вы всегда должны пытаться реализовать код наилучшим из известных вам способов, а не только так, как это всегда делалось.


Двойные апострофы будут переведены сервером sql в один апостроф, поэтому О'Рейли будет переведен на Name = 'O''Reilly'
Rune Grimstad

Так есть ли соответствующая функция для удаления апострофов, когда пользователь хочет увидеть свои данные?
quamrana

Незачем. Управляющая последовательность позволяет синтаксическому анализатору видеть одиночную кавычку, а не конец строки. Во время синтаксического анализа он воспринимается ''как литерал ', поэтому ваша строка будет рассматриваться внутри как последовательность символов O'Reilly. Это то, что БД будет хранить, извлекать, сравнивать с и т. Д. Если вы хотите показать пользователю его данные после того, как вы экранировали его, сохраните копию неэкранированной строки в приложении.
cHao

1

Вот пара статей, которые могут помочь вам убедить ваших коллег.

http://www.sommarskog.se/dynamic_sql.html

http://unixwiz.net/techtips/sql-injection.html

Лично я предпочитаю никогда не позволять никакому динамическому коду касаться моей базы данных, требуя, чтобы все контакты осуществлялись через sps (а не тот, который использует динамический SQl). Это означает, что ничего, кроме того, что я дал пользователям разрешение, не может быть сделано, и что внутренние пользователи (за исключением очень немногих с производственным доступом для административных целей) не могут напрямую обращаться к моим таблицам и создавать хаос, красть данные или совершать мошенничество. Если вы запускаете финансовое приложение, это самый безопасный способ.


1

Он может быть взломан, однако способ зависит от точных версий / патчей и т. Д.

Один из них уже упоминался - это ошибка переполнения / усечения, которую можно использовать.

Другое средство в будущем будет находить ошибки , похожие на другие базы данных - например , стек MySQL / PHP пострадали ускользающую проблему , потому что некоторые последовательности UTF8 может быть использованы для манипулирования функции замены - функция замены будет обмануто в введении символов инъекций.

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

Если у вас много устаревшего кода, метод replace можно использовать в качестве временного решения, чтобы избежать длительного переписывания и тестирования. Если вы пишете новый код, нет оправдания.


1

По возможности всегда используйте параметризованные запросы. Иногда даже простой ввод без использования каких-либо странных символов уже может создать SQL-инъекцию, если он не идентифицирован как ввод для поля в базе данных.

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


1

Я не видел, чтобы другие ответчики обращались к этой стороне вопроса «почему делать это самому - плохо», но рассмотрим атаку усечения SQL .

Также существует QUOTENAMEфункция T-SQL, которая может быть полезна, если вы не можете убедить их использовать параметры. Он улавливает множество (все?) Ускользнувших проблем qoute.


1

2 года спустя я вернулся ... Любой, кто считает параметры болезненными, может попробовать мое расширение VS, QueryFirst . Вы редактируете свой запрос в реальном файле .sql (Validation, Intellisense). Чтобы добавить параметр, вы просто вводите его прямо в свой SQL, начиная с '@'. Когда вы сохраняете файл, QueryFirst сгенерирует классы-оболочки, которые позволят вам выполнить запрос и получить доступ к результатам. Он найдет тип DB вашего параметра и сопоставит его с типом .net, который вы найдете в качестве входных данных для сгенерированных методов Execute (). Не может быть проще. Сделать это правильно радикально быстрее и проще, чем сделать это любым другим способом, а создание уязвимости SQL-инъекции становится невозможным или, по крайней мере, извращенно трудным. Есть и другие преимущества убийцы, такие как возможность удалять столбцы в вашей БД и сразу видеть ошибки компиляции в вашем приложении.

юридический отказ от ответственности: я написал QueryFirst


0

Вот несколько причин использовать параметризованные запросы:

  1. Безопасность - уровень доступа к базе данных знает, как удалять или избегать элементов, которые не разрешены в данных.
  2. Разделение проблем - мой код не отвечает за преобразование данных в формат, который нравится базе данных.
  3. Без избыточности - мне не нужно включать сборку или класс в каждый проект, который выполняет это форматирование / экранирование базы данных; он встроен в библиотеку классов.

0

Было несколько уязвимостей (я не могу вспомнить, какая это была база данных), связанных с переполнением буфера оператора SQL.

Я хочу сказать, что SQL-Injection - это больше, чем просто «уход от цитаты», и вы не знаете, что будет дальше.


0

Еще одно важное соображение - отслеживание экранированных и неэкранированных данных. Существует множество приложений, веб-сайтов и прочих, которые, похоже, не отслеживают должным образом, когда данные являются необработанными - Unicode, & -кодированными, отформатированными HTML и т. Д. Очевидно, что станет трудно отслеживать, какие строки ''закодированы, а какие нет.

Это также проблема, когда вы в конечном итоге меняете тип некоторой переменной - возможно, раньше это было целое число, а теперь это строка. Теперь у тебя проблема.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.