Предотвращаем атаки переполнения кэша в движке Joomla 3.x

archive view archive save

joomla.jpg Из предыдущих материалов мы знаем, что в движке Joomla 3.x и до Joomla 5.x входящие параметры в ссылках вовсе никак не фильтруются - это позволяет забомбить кэш добавляя в ссылку лишние параметры и/или случайным образом изменяя их значения.

В журналах сервера неоднократно встречались запросы по-ссылкам типа:

https://example.com/itemlist?start=5540&url=770&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&start=5540&format=feed&type=atom

Откуда боты берут такие ссылки, или создают их намеренно, - неизвестно. По-факту в кэш попадают все УРЛы, включая ссылки с дубликатами параметров и/или случайным образом изменёнными в них значениями.

Page Caching (плагин Система - Кэш) позволяет добавить исключения, которые реализованы в isExcluded() файла /plugins/system/cache/cache.php:

// Gets internal URI.
$internal_uri = '/index.php?' . JUri::getInstance()->buildQuery($this->app->getRouter()->getVars());

$internal_uri получается примерно таким: /index.php?Itemid=666&option=com_k2&view=item&id=6666:article-alias-here

К $internal_uri добавляется $this->_cache_key, содержащее запрошенный УРЛ типа https://example.com/itemlist?start=666, потом перебирается массив с исключениями и выполняется проверка их наличия в строке:

if (preg_match('#' . $exclusion . '#i', $this->_cache_key . ' ' . $internal_uri, $match))

Поиск исключений идёт одновременно по внешнему https://example.com/article-alias-here и внутреннему /index.php?Itemid=666&option=com_k2&view=item&id=6666:article-alias-here адресам.

Поэтому кэш полностью отключится при наличии в исключениях одного из таких слов и/или символов, как:

  • index
  • option
  • view
  • item
  • id
  • itemid
  • .
  • /
  • ?
  • &
  • =
  • -
  • :

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

  • \,
  • \\

В исключения можно добавить названия компонентов (com_something), ключевые слова из веб-адресов не требующий кэширования (страница билинга например), и ещё что угодно. Но, исключения не спасут от попадания в кэш страниц по-ссылкам с лишними параметрами или дубликатами параметров типа start=5540&start=5540.

Обратить внимание также следует и на то, что параметры в разных регистрах, как например Start=5540&stART=5540 и start=5540&start=5540, будут считаться разными ссылками.

Для решения данной проблемы на всём фронтенде сразу предлагается добавить фильтр для серверных переменных $_SERVER['QUERY_STRING'] и $_SERVER['REQUEST_URI'] разместив его в файле /defines.php

// PREVENT BOMBING CACHE WITH PARAMS DUBLICATE
if($_SERVER['QUERY_STRING']){
  $allowKeys = [
    'start' => '',
    'format' => '',
    'option' => '',
    'view' => '',
    'layout' => '',
    'limit' => '',
    'link' => '',
    'pop' => '',
    'print' => '',
    'task' => '',
    'template' => '',
    'tmpl' => '',
    'tpl' => '',
    'type' => '',
    'id' => '',
    'item' => '',
    'itemid' => '',
  ];
  //
  // Fix param in $_SERVER['QUERY_STRING']
  parse_str(strtolower($_SERVER['QUERY_STRING']), $queryStr);
  if (count($queryStr) > 0) {
    foreach ($queryStr as $key => $value) {
      // isset() faster instead array_key_exists()
      if (!isset($allowKeys[$key])) {
        unset($queryStr[$key]);
      } elseif (!empty($value) && preg_match('/[^\w\d\ \-:]+/i', $value)) {
        // if param value contain not word or not digit or not some symbol
        // then access deny
        //header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
        header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
        header('Content-Type: text/html');
        echo 'Invalid vaue (' . $value . ') for parameter (' . $key . ')!';
        exit;
      }
    }
  }
  $_SERVER['QUERY_STRING'] = http_build_query($queryStr);
  //
  // Fix param in $_SERVER['REQUEST_URI']
  $requestUri = parse_url($_SERVER['REQUEST_URI']);
  $_SERVER['REQUEST_URI'] = $requestUri['path'] . '?' . $_SERVER['QUERY_STRING'];
}

Данный PHP код:

  • Отбросит все параметры, которых нет в $allowKeys;
  • parse_str() попутно удалит все дубликаты параметров;
  • strtolower() переведёт всё в нижний регистр;
  • preg_match('/[^\w\d\ \-:]+/i', $value) для полноты счастья проверит значение на присутствие там запрещённых знаков и выдаст HTTP 403 или 404 если таковые обнаружатся;
  • http_build_query() соберёт параметры обратно в строку.

Добавленный в /defines.php PHP код будет выполнен в самом начале работы движка и приведёт в Божеский вид параметры строки запроса в $_SERVER['QUERY_STRING'] и $_SERVER['REQUEST_URI'].

Всего с десяток строк кода, а выгоды так цельный вагон.


Комментарии в блоге
Новое на форуме