Безопасное сохранение массива в поле MySQL

Тема в разделе "PHP", создана пользователем Denixxx, 7 ноя 2015.

Статус темы:
Закрыта.
Модераторы: latteo
  1. Denixxx

    Denixxx

    Регистр.:
    7 фев 2014
    Сообщения:
    247
    Симпатии:
    194
    Привет всем.
    Иногда перед любым разработчиком встает вопрос — как сохранить массив в поле MySQL.
    Это нужно редко и часто такое решение не совсем правильно, но всё-таки иногда нужно.
    Тут недавно в чатике предлагали такое решение — сериализовать и архивировать массив.
    Получится строка, которую можно сохранить в поле MySQL.
    Каюсь, сам недавно грешил таким.
    В чатике предложено такое решение: $data = gzcompress(serialize($arr));
    Где $data — строка для БД, $arr — массив для хранения
    Но, во-первых, такая строка требует ещё обработки mysql_real_escape_string
    Во-вторых, сжатая бинарная строка может содержать любые символы, в том числе и символы экранирования, кавычки и так далее, что делает нестабильным процесс распаковки.
    В-третьих, такая система делает невозможным использовать в значениях исходных массивов некоторых символов, например \
    Это может испортить контент.
    Поэтому вместо mysql_real_escape_string лучше использовать безопасное кодирование в base64
    Это увеличивает немного строку, зато делает безопасной и стабильной расшифровку и работает довольно быстро.

    В общем, предлагаются 2 функции, которые могут надежно упаковать и распаковать массив в/из строки для последующего использования в БД.
    PHP:
    function array_to_string_encode($arr) {
        if (empty(
    $arr) OR !is_array($arr)) return false;
        
    $data base64_encode(gzcompress(serialize($arr)));
        return 
    str_replace(array('+','/','='),array('-','_',''),$data);
    }

    function 
    array_from_string_decode($string) {
        if (empty(
    $string)) return false;
        
    $data str_replace(array('-','_'),array('+','/'),$string);
        
    $mod4 strlen($data) % 4;
        if (
    $mod4) {
            
    $data .= substr('===='$mod4);
        }  
        return 
    unserialize(gzuncompress(base64_decode($data)));
    }
    Предлагаю всех знающих людей поучаствовать в обсуждении и/или предложить свои варианты.
    Не сомневаюсь, что тема актуальная.
    //PS Буду рад, если найдется более красивое, краткое и быстрое решение.
     
    Последнее редактирование: 17 ноя 2015
    pozhisni нравится это.
  2. KillDead

    KillDead

    Регистр.:
    11 авг 2006
    Сообщения:
    884
    Симпатии:
    540
    Готов обсудить. Итак по пунктам:
    Почему это минус то? Ну, если переиначить и посмотреть твой пример
    "без экранирования строка требует упаковки через басе64 и дополнительную замену через str_replace". По моему чем проще - тем лучше.
    Да и вообще практика общения с мускулом такая, что "при любой ситуации экранируй данные" и об этом твердят везде, не понимаю почему экранирование тогда, когда это нужно - вред.

    Эм, я тут в ступоре - это почему небезопасно? Где опасность? Ну, тогда и в файл сохранять можно только через base64, ведь там могут быть символы экранирования. А, ещё сервер поступает опасно, ведь сжимает, вывод перед отдачей, а вдруг на странице слэш есть...

    Опять в ступоре. Можно их сохранять, более того и утф, и многобайтовые символы можно и спец символы. Да, я сталкивался над каким то непонятным багом, когда проблема была из-за количества слэшей, чтото вроде \\\\\\\\\\\' класс мускула валился с ошибкой, но это решилось фиксом класса. Можно реальный пример данных привести, когда символ потерялся? Может ты сохранял чтото вроде $data = "A \\ B" и ВНЕЗАПНО увидел всего один \.

    Пока что плюсы использования твоего метода:
    1. Размер становится больше
    2. ЦП нагружается больше
    3. Решается 0 проблем с безопасность

    + Небольшую ремарку от меня
    Хранить сжатый сериализованный массив надо только тогда когда это ППЦ как оправдано. К примеру у меня минимум 1кк строк и каждая содержит по ~20 килобайт таких массивов. Вышла база в 36 гигобайт. И это минимум. Для меня это стало проблемой по этому сжатие- одно из решений. Если у вас не такие объёмы- не нужно париться и предпринимать чтото подобное, обычного serialize будет достаточно.
     
    Последнее редактирование: 7 ноя 2015
    mrwad и latteo нравится это.
  3. javx

    javx

    Регистр.:
    28 авг 2015
    Сообщения:
    528
    Симпатии:
    246
    Используй mysqli там не нужно парится по поводу инъекций. А предложение кодировать в base64 это ужс.
     
    ZiX нравится это.
  4. Denixxx

    Denixxx

    Регистр.:
    7 фев 2014
    Сообщения:
    247
    Симпатии:
    194
    Вырезание всех опасных символов всегда лучше и надежней экранирования данных. Base64 предполагает кодирование в полностью безопасных символах. Кроме + и /, которые приходится заменять на безопасные.

    В файл можно сохранять просто через serialize, и больше ничего — это вполне безопасно.

    Сериализованные данные нельзя запихнуть в неизменном виде в БД — там могут быть кавычки например. Поэтому нужно экранировать.
    А экранирование/разэкранирование стандартной mysql_real_escape_string сериализованных данных нормально не работает. А функции, обратной mysql_real_escape_string вообще не существует, что автоматически означает, что данные могут портиться и вернуть их назад будет нельзя.
    Особенно с данными, в которых могут быть обратные слеши, кавычки, -- (в MySQL это комментарий) и прочие радости.
    Проверено на опыте. Когда столкнетесь с этим, вспомните меня.
     
    Последнее редактирование: 7 ноя 2015
  5. Denixxx

    Denixxx

    Регистр.:
    7 фев 2014
    Сообщения:
    247
    Симпатии:
    194
    Вот простой пример:
    PHP:
    $arr= array(
    '10.1" SLIM grade A от 5шт  по 35$',
    '14.0" LED grade A   от 5 шт по 47$',
    '15.6" SLIM grade \A от 5шт  по 46$',
    'А тут внезапно подкрался пипец',
    '_==+-\\#4$:"'
    );

    $data mysql_real_escape_string(gzcompress(serialize($arr)));
    echo 
    $data.'<br />';
    //Пробуем получить данные назад
    $original=unserialize(gzuncompress(stripslashes($data)));
    print_r($original);
    Результат:
    Как видим, данные потеряны безвозвратно. Со мной случалось такое не раз и не два.

    ЗЫ. При обработке base64 данные увеличиваются максимум на 25%, что доказано тестами.
    Если учесть, что использование gz сжимает текстовые данные в среднем раз в 10, выигрыш очевиден.
     
    Последнее редактирование: 7 ноя 2015
  6. KillDead

    KillDead

    Регистр.:
    11 авг 2006
    Сообщения:
    884
    Симпатии:
    540
    :) Так, уже хорошо. Есть на чем поразмыслить.

    1. А ты пробовал выполнить этот пример с БД? А ты попробуй)
    2. Чисто по этому примеру - Что экранирует mysql_real_escape_string и что делает stripslashes.
     
    pozhisni и latteo нравится это.
  7. Denixxx

    Denixxx

    Регистр.:
    7 фев 2014
    Сообщения:
    247
    Симпатии:
    194
    Ну мне и лень и некогда сейчас создавать таблицу и проделывать все эти штуки. Потому что ранее я уже «провел эксперименты» на рабочих сайтах.
    Поиск и создание этого решения — было суровой необходимостью, и родилось не за 5 минут.
    Может как-нибудь позже, как будет время, прогоню через БД.
     
  8. KillDead

    KillDead

    Регистр.:
    11 авг 2006
    Сообщения:
    884
    Симпатии:
    540
    Сурово. Ладно, попробуем разобраться почему так происходит в ТВОЁМ примере. Идём на офф страницу где есть функция Перейти по ссылке узнаём что на самом деле экранирует mysql_real_escape_string (если документация не помогает) и делаем обратную функцию
    Код:
    function mysql_escape_decode($inp) {
        if (is_array($inp))
            return array_map(__METHOD__, $inp);
    
        if (!empty($inp) && is_string($inp)) {
            return str_replace(array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), $inp);
        }
    
        return $inp;
    }
    $arr= array(
    '10.1" SLIM grade A от 5шт по 35$',
    '14.0" LED grade A от 5 шт по 47$',
    '15.6" SLIM grade \A от 5шт по 46$',
    'А тут внезапно подкрался пипец',
    '_==+-\\#4$:"'
    );
    
    $data = mysql_real_escape_string(gzcompress(serialize($arr)));
    echo $data.'<br />';
    //Пробуем получить данные назад
    $original=unserialize(gzuncompress(mysql_escape_decode($data)));
    print_r($original);
    
    Код:
    x�mͱ\n�@�W    g7��ٻ�)�:������\"hq�G�(J�+��ȴ� 8$C��?    \Z\\�P��(�t��a��t��\'�*l\n��y=�\0�8\"��b�%�JQ���6P���Ve�c\\��M���+�1>\n:�M��l<SN]�Ĺ��e�.t�k>=���Ӎ\'���Cs�R(FaXo�5�����f0<br />Array
    (
        [0] => 10.1" SLIM grade A от 5шт  по 35$
        [1] => 14.0" LED grade A   от 5 шт по 47$
        [2] => 15.6" SLIM grade \A от 5шт  по 46$
        [3] => А тут внезапно подкрался пипец
        [4] => _==+-\#4$:"
    )
    
    Вотзефак? Работает?
     
    latteo нравится это.
  9. KillDead

    KillDead

    Регистр.:
    11 авг 2006
    Сообщения:
    884
    Симпатии:
    540
    + Разгоняемся. На скорую руку, возможно есть ошибки при конвертации символов

    1. Берём диапазоны утф Перейти по ссылке
    2. Берём chr которая поддерживает мультибайтовые символы
    3. Перебираем и пихаем в массив, делаем слепок мд5.
    4. Конвертируем и проверяем.


    Код:
    function mysql_escape_decode($inp) {
        if (is_array($inp))
            return array_map(__METHOD__, $inp);
    
        if (!empty($inp) && is_string($inp)) {
            return str_replace(array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), $inp);
        }
    
        return $inp;
    }
    
    function unichr($u) {
        return mb_convert_encoding('&#' . intval($u) . ';', 'UTF-8', 'HTML-ENTITIES');
    }
       
    $arr =array();
    for ($i = hexdec ('0000'); $i < hexdec ('FFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('10000'); $i < hexdec ('1FFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('20000'); $i < hexdec ('2FFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('30000'); $i < hexdec ('3FFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('E0000'); $i < hexdec ('EFFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('F0000'); $i < hexdec ('FFFFF'); $i++) {
        $arr[] = unichr($i) ;
    }
    for ($i = hexdec ('100000'); $i < hexdec ('100000'); $i++) {
        $arr[] = unichr($i) ;
    }
    
    $md5_original = md5(serialize($arr) . json_encode($arr) . count($arr));
    echo 'Хеш исходного массива ' . $md5_original . '<br>';
    
    
    $dataGzip = gzcompress(serialize($arr));
    echo 'Размер после сжатия ' . strlen($dataGzip) . '<br>';
    
    $data = mysql_real_escape_string($dataGzip);
    //Пробуем получить данные назад
    $original = unserialize(gzuncompress(mysql_escape_decode($data)));
    
    $md5_result = md5(serialize($original) . json_encode($original) . count($original));
    echo 'Хеш исходного массива ' . $md5_result . '<br>';
    if ($md5_original != $md5_result) {
        echo 'Произошла ошибка!!!!  <br>';
    } else {
        echo 'У нас все здоровы быки и коровы столбы и заборы <br>';
    }
    

     
    latteo и Denixxx нравится это.
  10. Denixxx

    Denixxx

    Регистр.:
    7 фев 2014
    Сообщения:
    247
    Симпатии:
    194
    Спасибо, завтра на работе посмотрю. Всё-таки я для парсинга брал синтетический пример.
    А на выходных не удается не то что разобраться, но редко и за компом посидеть.
    Возможно, если и сейчас что-то работает, то позже перестанет.
    Навскидку, если в исходном массиве будет что-то из этого списка:
    PHP:
    array('\\\\''\\0''\\n''\\r'"\\'"'\\"''\\Z');
    то не должно оно нормально работать.
    Просто потому, что невозможно предугадать, что на самом деле вырезал mysql_real_escape_string — мне кажется, это вырезалось необратимо. Может быть также, что mysql_real_escape_string ведет себя по-разному в разных версиях MySQL.
    Впрочем, завтра проверю, возможно я и не прав.
     
Статус темы:
Закрыта.