Разработка

Создаем безопасные расширения для Joomla

Joomla
Добавление в избранное
Сохранить
1
Создаем безопасные расширения для Joomla

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

Содержание

Защита от прямого доступа

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

http://www.example.com/index.php?option=com_yourcomponent

злоумышленник может сделать так:

http://www.example.com/components/com_yourcomponent/yourcomponent.php

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

class myClass
{
 
    // здесь какие-то функции
 
}
 
function myFunction()
{
 
    // здесь какой-то код
 
}

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

Чтобы защитить ваше расширение от прямого доступа, просто разместите вот эту строку кода в начале PHP файла, который исполняет код:

defined('_JEXEC') or die();

Это строка должна быть в начале каждого PHP файла, который исполняет код! Если вы не уверены, исполняет ли ваш файл код, все равно используйте эту строку. Хуже не будет.

Защита от PHP-инклюдинга

PHP-инклюдинг (PHP file inclusion) – это уязвимость, позволяющая выполнить произвольный PHP-код на уязвимом сервере.

Предположим, что вы используете вот такой код для подключения файла разметки:

$layout = $_GET['layout'];
include($layout);

Тогда взломщик может использовать URL в параметре layout для подключения удаленного файла, например вот так:

http://www.example.com/com_yourcomponent/views/yourview?layout=http://www.bad.site/code.php

Как же от этого защититься?

Самым простым способом будет использование абсолютного пути при подключении файла, чтобы исключить неоднозначность для PHP при поиске файла:

$layout = $_GET['layout'];
include JPATH_SITE . '/components/com_yourcomponent/views/tmpl/' . $layout;

или

include JPATH_ADMINISTRATOR . '/components/com_yourcomponent/views/tmpl/' . $layout;

Константы JPATH_SITE и JPATH_ADMINISTRATOR недоступны для манипуляции злоумышленником.

Однако лучшим способом будет фильтрация пользовательского ввода, используя класс JInput, а не чистый массив $_GET.

$input = JFactory::getApplication()->input;
 
$layout = $input->get('layout', 'default');
include JPATH_SITE . '/components/com_yourcomponent/views/tmpl/' . $layout;

По умолчанию JInput применяет к пользовательскому вводу фильтр CMD, который разрешает только следующие символы: a-z, 0-9, нижнее подчеркивание, точку и тире. Такая фильтрация предотвращает также обход по директориям, когда взломщик пытается манипулировать путем для включения файла на этом же сервере (локальное подключение файла). Если вы хотите использовать другие фильтры, то более подробное описание класса JInput вы можете найти в нашей документации.

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

Итак, чтобы обезопасить ваш код от удаленного подключения файлов, вы должны убедиться в том, что вы не используете непроверенный пользовательский ввод при включении файлов. Вы никогда не должны использовать чистые массивы $_GET и $_POST, вместо этого используйте класс JInput для фильтрации пользовательского ввода.

Будьте предельно осторожны с вызовами функций, которые подключают файлы, такие как: include, require, include_once, require_once, fopen. Если вам действительно необходимо подключать файлы с именами переменных, убедитесь в том, что вы валидировали все эти переменные, а также пытайтесь ограничить диапазон разрешенных значений.

Защита от SQL-инъекций

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

Взгляните на этот код:

$value = $_GET['value'];
 
$database->setQuery("SELECT * FROM #__mytable WHERE id = $value");

Взломщик может передать строку '1 OR 1', и запрос будет иметь следующий вид:

SELECT * FROM #__mytable WHERE id = 1 OR 1

Этот запрос вернет все строки из таблицы #__mytable! Это очень простой пример возможного эксплойта, существует множество других.

Одним из самых распространённых заблуждений среди разработчиков является то, что SQL-инъекция может использоваться только для манипулирования результатами в оригинальном SQL-запросе, поэтому не такой уж и большой риск, если таблица не важна. Это не так! Включая в свой запрос команду UNION SELECT, злоумышленник сможет получить доступ к любой таблице базы данных. Так что это очень большой риск!

Не будем вдаваться в подробности, так как тема SQL-инъекций достаточно подробно освещена в интернете. Лучше поговорим о защите от этого метода взлома.

А защита тут одна - фильтрация. Фильтруйте весь пользовательский ввод перед тем, как использовать его в SQL-запросе. Применяйте метод quote(), ко всем строковым переменным, используемых в SQL-запросах:

$db = JFactory::getDbo();
 
$string = $db->quote($string);

Также используйте приведение типов для всех числовых переменных, используемых в SQL-запросах:

(int) $value;

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

Возьмите за правило всегда получать любые данные из пользовательского ввода, используя класс JInput, а не чистые массивы $_GET и $_POST. Однако вы не всегда можете положиться на JInput для предотвращения SQL-инъекций. Некоторые инъекционные атаки могут быть выполнены, используя такие символы, которые при определенных обстоятельствах могут быть приняты без проблем. Так что самым надежным способом является применение метода quote() и привидения типов.

Защита от XSS

Межсайтовый скриптинг (Cross Site Scripting - XSS) означает выполнение скриптового кода в браузере. Такие атаки могут быть использованы для кражи сессионной куки пользователя, что позволит злоумышленнику выдать себя за залогиненого пользователя. Будьте осторожны при выводе пользователю непроверенного ввода. Вот такой код может быть очень опасным для ваших посетителей:

echo $_REQUEST['value'];

И снова на помощь приходит фильтрация и класс JInput:

$input = JFactory::getApplication()->input;
 
$value = $input->get('value','','html');
echo $value;

Этот код удалит из значения все HTML теги и атрибуты.

Если вам вдруг понадобиться хранить HTML, который вводил пользователь, и далее выводить его, то вы можете использовать PHP-функцию htmlspecialchars():


$value = htmlspecialchars($value);
 
echo $value;

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

Если вы используете пользовательский ввод для получения значения для javascript кода, то вы должны быть еще более осторожны, так как простая очистка от HTML не будет достаточной. Например, у нас есть вот такой код:

<script type="text/javascript">
    var colour = <?php echo $input->get('colour', 'blue', 'alnum'); ?>;
</script>

В этом случае был использован фильтр 'alnum', который удаляет всё, оставляя только символы a-z, 0-9. Здесь нужен такой строгий фильтр, иначе злоумышленник может сделать вот так:

blue%3B%20window.alert(document.cookie)%3B

Если использовать фильтр, который удаляет только HTML теги, то это приведет к следующему:

<script type="text/javascript">
    var colour = blue;
    window.alert(document.cookie);
</script>

Защита от CSRF-атак

Межсайтовая подделка запроса (Сross Site Request Forgery - CSRF) позволяет злоумышленнику выполнить какие-либо действия на уязвимом сайте от лица жертвы.

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

http://example.com/administrator/index.php?option=com_yourcomponent&task=deleteall

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

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

<?php echo JHtml::_('form.token'); ?>

Далее вы делаете проверку этого токена (например, в контроллере):

JSession::checkToken() or jexit('Invalid Token');

Избегайте открытых прав на файлы и папки

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

Если поискать в интернете обсуждения на эту тему, то можно увидеть, что некоторые с этим не согласны, потому что PHP-скрипты как правило не могут быть исполнены при правах ниже 0644 для файлов и 0755 для папок. Но этот аргумент совершенно игнорирует не-PHP файлы. Межсайтовый скриптинг обычно совершается через вредоносные HTML и JavaScript, а иногда через зараженные Flash видео и Java апплеты.

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

Если же вы хотите избежать проблем с правами на папки, используйте chmod для временного изменения прав, а потом возвращайте их обратно. Для этого можно воспользоваться методом setPermissions($path, $filemode = '0644', $foldermode = '0755') класса JPath. Например:

$path = JPATH_COMPONENT . '/uploads/image.jpg';
 
if (JPath::canChmod($path))
{
    JPath::setPermissions($path, 0666, 0777);
}

Проверяйте загружаемые файлы

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

Для того чтобы обезопасить имя загружаемого файла, можно воспользоваться методом makeSafe($file) класса JFile:

$safeName = JFile::makeSafe($file);

Метод удаляет:

  • точку в начале имени файла
  • точки, стоящие рядом
  • все символы кроме латиницы, точки, цифр, нижнего подчеркивания, пробела и тире

Для проверки расширения файла вы можете использовать метод getExt($file):

$extension = JFile::getExt($file);
$whitelist = array('gif', 'jpeg', 'jpg', 'png');
 
foreach ($whitelist as $item)
{
    if (!preg_match("/$item\$/i", $extension)
    {
        echo 'Неверный формат изображения! Доступные для загрузки форматы: ' . implode(',', $whitelist);   
    }
}

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

$fileInfo = getimagesize($file);
$mimeType = $fileInfo['mime'];
 
$allowedMime = 
    array(
        'image/gif',
        'image/jpeg',
        'image/png'
    );
 
if (!in_array($mimeType, $allowedMime)) 
{
    echo 'Неверный формат изображения! Доступные для загрузки форматы: gif, jpg, png';   
}

Еще одним действенным средством является переименование загружаемого файла, например вот так:

$newFileName = sha1($file) . '_' . rand(0,10000) . '.' . JFile::getExt($file);

Далее вы сохраняете его название в базе данных и вызываете, где это необходимо, с помощью PHP-кода.

К сожалению и это не всегда спасает. Тогда можно воспользоваться защитой на стороне сервера. Ниже приведен листинг файла .htaccess, который защитит директорию, в которую выполняются публичные загрузки. Директивы также будут распространяться на подпапки:

# По умолчанию запрещаем доступ ко всем файлам
Order Allow,Deny
 
# Белый список расширений
<FilesMatch "\.(jp?g|png|gif)$" >
  Order Deny,Allow
</FilesMatch>
 
# Запрещаем выполнение скриптов
Options -ExecCGI
php_flag engine off
SetHandler none

Как правило, эти директивы работают на всех хостингах. Но если директива SetHandler none не сработает, то можно поменять её на SetHandler default-handler. Обычно default-handler - это простая отдача файлов без обработки. Но если она установлена в режим парсера, то она бесполезна.

Можно также воспользоваться директивой RemoveHandler, но здесь мы не всегда можем быть уверены, что охватили все расширения:

RemoveHandler .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp .htm .html .wml

Для веб-сервера nginx придется указывать все папки, которые вы хотите защитить, а также нежелательные расширения файлов:

location ~* ^/(upload|images)/.*\.(php|phtml|php5)$ 
{
   deny all;
}

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

Проверяйте привилегии пользователей

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

Также убедитесь в том, что вы не показываете пользователю информацию, к которой у него не должно быть доступа. Для получения уровней просмотра, к которым пользователь имеет доступ, вы можете использовать метод getAuthorisedViewLevels() класса JUser:

$user = JFactory::getUser();
 
$groups = implode(',', $user->getAuthorisedViewLevels());

Ниже приведен пример простого SQL-запроса, который принимает во внимание права категории:

SELECT * FROM #__contact_details AS c
LEFT JOIN #__categories AS cat ON cat.id = c.catid
WHERE (c.name LIKE '%$text%')
AND c.published = 1
AND cat.published = 1
AND c.access IN ($groups)
AND cat.access IN ($groups)

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

Joomla ACL – это большая тема для обсуждения, поэтому мы не будем рассматривать её в рамках этого материала. На нашем сайте вы можете найти полный обзор Joomla ACL, а также документацию по использованию ACL в компоненте.

Dmitry Rekun
Работаю в банковской сфере, а с веб-разработкой (непосредственно с Joomla) столкнулся в 2007 году. Теперь это моё хобби, а в редких случаях и вторая работа. Какое-то время вёл свой блог, но решил попробовать работать в команде. И вот c 2012 года я здесь :)

Подпишитесь на рассылку новостей CMScafe