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

Статус
В этой теме нельзя размещать новые ответы.

Denixxx

Мой дом здесь!
Регистрация
7 Фев 2014
Сообщения
244
Реакции
216
Привет всем.
Иногда перед любым разработчиком встает вопрос — как сохранить массив в поле 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 Буду рад, если найдется более красивое, краткое и быстрое решение.
 
Последнее редактирование:
Готов обсудить. Итак по пунктам:
Но, во-первых, такая строка требует ещё обработки mysql_real_escape_string
Почему это минус то? Ну, если переиначить и посмотреть твой пример
"без экранирования строка требует упаковки через басе64 и дополнительную замену через str_replace". По моему чем проще - тем лучше.
Да и вообще практика общения с мускулом такая, что "при любой ситуации экранируй данные" и об этом твердят везде, не понимаю почему экранирование тогда, когда это нужно - вред.

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

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

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

+ Небольшую ремарку от меня
Хранить сжатый сериализованный массив надо только тогда когда это ППЦ как оправдано. К примеру у меня минимум 1кк строк и каждая содержит по ~20 килобайт таких массивов. Вышла база в 36 гигобайт. И это минимум. Для меня это стало проблемой по этому сжатие- одно из решений. Если у вас не такие объёмы- не нужно париться и предпринимать чтото подобное, обычного serialize будет достаточно.
 
Последнее редактирование:
Используй mysqli там не нужно парится по поводу инъекций. А предложение кодировать в base64 это ужс.
 
  • Нравится
Реакции: ZiX
Да и вообще практика общения с мускулом такая, что "при любой ситуации экранируй данные" и об этом твердят везде, не понимаю почему экранирование тогда, когда это нужно - вред.
Вырезание всех опасных символов всегда лучше и надежней экранирования данных. Base64 предполагает кодирование в полностью безопасных символах. Кроме + и /, которые приходится заменять на безопасные.

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

Если у вас не такие объёмы- не нужно париться и предпринимать чтото подобное, обычного serialize будет достаточно.
Сериализованные данные нельзя запихнуть в неизменном виде в БД — там могут быть кавычки например. Поэтому нужно экранировать.
А экранирование/разэкранирование стандартной mysql_real_escape_string сериализованных данных нормально не работает. А функции, обратной mysql_real_escape_string вообще не существует, что автоматически означает, что данные могут портиться и вернуть их назад будет нельзя.
Особенно с данными, в которых могут быть обратные слеши, кавычки, -- (в MySQL это комментарий) и прочие радости.
Проверено на опыте. Когда столкнетесь с этим, вспомните меня.
 
Последнее редактирование:
Вот простой пример:
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);
Результат:
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=���Ӎ\'���Cs�R(FaXo�5�����f0
Warning: gzuncompress() : data error in тут_много_букаф.php(1335) : eval()'d code on line 12
Как видим, данные потеряны безвозвратно. Со мной случалось такое не раз и не два.

ЗЫ. При обработке base64 данные увеличиваются максимум на 25%, что доказано тестами.
Если учесть, что использование gz сжимает текстовые данные в среднем раз в 10, выигрыш очевиден.
 
Последнее редактирование:
Как видим, данные потеряны безвозвратно. Со мной случалось такое не раз и не два.
:) Так, уже хорошо. Есть на чем поразмыслить.

1. А ты пробовал выполнить этот пример с БД? А ты попробуй)
2. Чисто по этому примеру - Что экранирует mysql_real_escape_string и что делает stripslashes.
 
А ты пробовал выполнить этот пример с БД? А ты попробуй)
Ну мне и лень и некогда сейчас создавать таблицу и проделывать все эти штуки. Потому что ранее я уже «провел эксперименты» на рабочих сайтах.
Поиск и создание этого решения — было суровой необходимостью, и родилось не за 5 минут.
Может как-нибудь позже, как будет время, прогоню через БД.Для просмотра ссылки Войди или Зарегистрируйся
 
Ну мне и лень и некогда сейчас создавать таблицу и проделывать все эти штуки. Потому что ранее я уже «провел эксперименты» на рабочих сайтах.
Поиск и создание этого решения — было суровой необходимостью, и родилось не за 5 минут.
Может как-нибудь позже, как будет время, прогоню через БД.

Сурово. Ладно, попробуем разобраться почему так происходит в ТВОЁМ примере. Идём на офф страницу где есть функция Для просмотра ссылки Войди или Зарегистрируйся узнаём что на самом деле экранирует 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$:"
)
Вотзефак? Работает?
 
+ Разгоняемся. На скорую руку, возможно есть ошибки при конвертации символов

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>';
}


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