Код для защиты GIF, PNG и JPEG (к предыдущей статье)
В коментариях к предыдущей заметке о моём предложении по защите графических форматов от внедрённого кода один из читателей попросил привести код, который показывал бы как моё предложение работает.
Для форматов GIF, JPEG и PNG я за полчаса написал следующее (язык — PHP 5.4 и выше):
<?
// //bolknote.ru август 2012
// Защищаем изображения, возращает false в случае неудачи
function ProtectPicture($inname, $outname)
{
// типы изображения, которые мы можем защитить и функции защиты
$enabled = [
IMAGETYPE_JPEG => 'Jpeg',
IMAGETYPE_PNG => 'Png',
IMAGETYPE_GIF => 'Gif',
];
if (list(,, $type) = @getimagesize($inname)) {
if (isset($enabled[$type])) {
if ($fi = @fopen($inname, 'rb')) {
flock($fi, LOCK_SH);
if ($fo = @fopen($outname, 'wb')) {
flock($fo, LOCK_EX);
// если все файлы удалось открыть, то вызываем функцию защиты
$ret = call_user_func('ProtectPicture'.$enabled[$type], $fi, $fo, filesize($inname));
fclose($fo);
fclose($fi);
if (!$ret) {
unlink($outname);
}
return $ret;
} else {
fclose($fi);
}
}
}
}
return false;
}
/* Private functions */
function ProtectPictureJpeg($fi, $fo, $controllen)
{
// заголовок JPEG
if (!@fwrite($fo, fread($fi, 2))) return false;
$protect = '<?php __halt_compiler();';
$length = pack('n', strlen($protect) + 2);
// дописываю секцию COM (comment)
if (!@fwrite($fo, "\xFF\xFE$length$protect")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 2) return false;
return true;
}
function ProtectPicturePng($fi, $fo, $controllen)
{
// заголовок PNG
if (!@fwrite($fo, fread($fi, 8))) return false;
// пропускаем кусок IHDR (он должен быть первым))
$len = unpack('N', $lenbin = fread($fi, 4))[1] + 8;
$ihdr = $lenbin . fread($fi, $len);
// Смотрим, не написал ли в этот кусок хакер свой код
if (($pos = strpos($ihdr, '<?')) !== false) {
// в стандарте перечислены поля, которые есть в этом куске — там должно быть 13 байт (+4 — длина)
if ($len != 17) return false;
// эта комбинация может встретиться только в первых восьми байтах заголовка — дальше поля не
// могут принимать такие большие значения
if ($pos > 11) return false;
// если разрешены короткие теги, нужно присмотреться к этому коду
// в столь ограниченном пространстве вред можно натворить только через запуск
// чего-то внешнего
if (ini_get('short_open_tag') && strpos($ihdr, '`', $pos + 2) !== false) {
return false;
}
}
if (!@fwrite($fo, $ihdr)) return false;
// кусок iTXt (text) с нашим кодом
$protect = "<?php __halt_compiler();";
$length = pack('N', strlen($protect));
$sum = pack('N', sprintf('%u', crc32($protect)));
if (!@fwrite($fo, "{$length}iTXt{$protect}{$sum}")) return false;
// остаток файла
if (@stream_copy_to_stream($fi, $fo) != $controllen - 8 - 4 - $len) return false;
return true;
}
function ProtectPictureGif($fi, $fo, $controllen)
{
$protect = "<?php __halt_compiler();";
// заголовок GIF
if (!@fwrite($fo, fread($fi, 6))) return false;
// поле Logical Screen Descriptor
$lsd = fread($fi, 7);
$flag = ord($lsd[4]);
// указана ли Global Color Table?
if ($flag & 128) {
$pixel = 1 + ($flag & 0b111);
$colors = pow(2, $pixel);
$gctlen = $colors * 3;
// читаем Global Color Table
$gct = fread($fi, $gctlen);
// не вставил ли уже туда злоумышленник „<?“?
// меняем у такой комбинации последний бит — это малозаметно,
// безопасность важнее!
if (strpos($gct, '<?') !== false) {
$gct = str_replace('<', '=', $gct);
}
if (!@fwrite($fo, $lsd . $gct)) return false;
} else {
$gctlen = 0;
if (!@fwrite($fo, $lsd)) return false;
}
// пишем комментарий с куском защиты
fwrite($fo, "!\xfe" . chr(strlen($protect)) . $protect . "\0");
if (@stream_copy_to_stream($fi, $fo) != $controllen - 6 - 7 - $gctlen) return false;
return true;
}
В коде есть два места где я ищу не встретилась ли комбинация <? раньше, чем я вставляю защиту — это в заголовочном куске (IHDR) в ПНГ и в глобальной таблице цветов у ГИФа.
В случае ПНГ я анализирую заголовок, чтобы понять как там оказалась эта комбинация, она валидна только в первых восьми байтах (на зло остаётся всего 6 байт), кроме того, в этом случае я контролирую размер заголовка, в стандарте там указано 13 байт, хотя, кажется, размер не ограничивается. В общем, в шести байтах особо не разгуляешься, всё что я придумал туда засунуть — `rm *`, т. е. очистка текущего каталога. На этот случай, я контролирую появление символа `. На мой взгляд, вероятность встретить в нормальном ПНГ в заголовке <? после которого идёт ` ничтожно мала.
С ГИФом проще — меняю младший бит у цвета, это вряд ли будет заметно, а безопасность важнее.
Женя, fclose, начиная с версии 5.3.2, больше не снимает блокировки.
Комментарий для ninjacolumbo.ya.ru:
Странно, но у меня этот код без проблем выполняется (PHP 5.4.5):
Комментарий для Евгения Степанищева:
Действительно странно %)
Комментарий для ninjacolumbo.ya.ru:
Это же ты, наверное, в комментарии вычитал, да? Наверное соврали или баг был.
Комментарий для Евгения Степанищева:
Ну я вообще всегда явно снимал блокировки, а тут полез почитать про fclose и увидел такое %)
Спасибо. Надеюсь права/лицензия на использования открытые :)
Комментарий для kurapov.name:
Открытые :)
Извините за глупый вопрос, а не перегрузит ли такой метод защиты сервер при загрузке большого количества картинок одновременно?
И вот ещё мне приходит на ум (простите, если глупо), а что если в картинку негодяй вставит код не на php, а на каком-либо другом серверном языке. Тогда Ваша защита к сожалению окажется бесполезной?
Комментарий для Eblinkoff:
Каким образом?
А у вас сервер выполняет код на любом серверном языке или настроен, всё-таки, на какой-то один?