Как найти и удалить скрытые внешние ссылки в Joomla 5 и 6

Проверка внешних ссылок в Joomla 5 и 6

Внешние ссылки сами по себе не являются проблемой для сайта. Они естественны для веба: мы ссылаемся на официальную документацию, сервисы, источники и полезные материалы. Проверять нужно не «все внешние ссылки подряд», а те, которые появились без вашего ведома, ведут на устаревшие или сомнительные домены, остались от старой рекламы либо были встроены в шаблон, модуль или другой код.

В старых инструкциях для такой проверки часто предлагали Xenu Link и поиск всех вхождений base64_decode. Для Joomla 5 и 6 такой подход уже неудобен и может привести к ошибкам. Ни Xenu, ни другие сторонние программы здесь не обязательны: достаточно phpMyAdmin, консоли браузера и файлового менеджера хостинга.

Что именно стоит искать

Проверка особенно полезна в нескольких ситуациях:

  • после установки или обновления шаблона, модуля, плагина или компонента;
  • после восстановления сайта или устранения заражения;
  • если в Search Console, антивирусе или исходном коде появились незнакомые домены;
  • если на сайте раньше использовались рекламные и партнёрские блоки;
  • если нужно провести ревизию старого контента.

Главная задача — понять где хранится ссылка и выводится ли она на сайте сейчас. Один и тот же URL может находиться в материале Joomla, в HTML-модуле, в файле расширения или всего лишь в кеше.

Поиск внешних ссылок в материалах через phpMyAdmin

Основной текст материалов Joomla хранится в таблице #__content, в полях introtext и fulltext. Поэтому массовую проверку можно выполнить непосредственно в базе данных.

Для начала можно вывести первое абсолютное внешнее URL из каждого материала:

SQL — поиск внешних URL
SELECT
    id,
    title,
    REGEXP_SUBSTR(
        CONCAT_WS(' ', introtext, `fulltext`),
        'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://[^"''<>[:space:]]+'
    ) AS external_url
FROM PREFIX_content
WHERE CONCAT_WS(' ', introtext, `fulltext`)
      REGEXP 'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://[^"''<>[:space:]]+'
ORDER BY id;

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

SQL-запрос для списка уникальных внешних доменов

В двух местах замените example\\.com на собственный домен. Точки в имени домена нужно экранировать как \\..

SQL — уникальные внешние домены
WITH RECURSIVE
src AS (
    SELECT
        id,
        title,
        CONCAT_WS(' ', introtext, `fulltext`) AS rest
    FROM PREFIX_content
    WHERE CONCAT_WS(' ', introtext, `fulltext`)
        REGEXP 'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
),
links (id, title, url, rest, n) AS (
    SELECT
        id,
        title,
        REGEXP_SUBSTR(
            rest,
            'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
        ),
        SUBSTRING(
            rest,
            REGEXP_INSTR(
                rest,
                'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
            ) + CHAR_LENGTH(
                REGEXP_SUBSTR(
                    rest,
                    'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
                )
            )
        ),
        1
    FROM src

    UNION ALL

    SELECT
        id,
        title,
        REGEXP_SUBSTR(
            rest,
            'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
        ),
        SUBSTRING(
            rest,
            REGEXP_INSTR(
                rest,
                'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
            ) + CHAR_LENGTH(
                REGEXP_SUBSTR(
                    rest,
                    'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
                )
            )
        ),
        n + 1
    FROM links
    WHERE n < 100
      AND rest REGEXP
        'href[[:space:]]*=[[:space:]]*["'']\\Khttps?://(?!(?:[a-z0-9-]+\\.)*example\\.com(?:[/:?#]|$))[^"''<>[:space:]]+'
)
SELECT
    LOWER(REGEXP_SUBSTR(url, 'https?://\\K[^/:?#]+')) AS domain,
    COUNT(*) AS links_count,
    COUNT(DISTINCT id) AS articles_count
FROM links
WHERE url <> ''
GROUP BY domain
ORDER BY articles_count DESC, links_count DESC, domain;

Список внешних доменов и количество ссылок в материалах Joomla

На реальном сайте такой запрос может вернуть десятки или сотни доменов. Это не означает, что все они вредоносные. Сначала смотрите на контекст: официальные сайты Joomla, Google, сервисы, о которых рассказывает статья, и старые партнёрские площадки могут быть вполне объяснимыми. Подозрение вызывают прежде всего неизвестные домены, которые повторяются в несвязанных материалах или которые вы точно не добавляли.

Проверяем HTML-модули Joomla

Внешняя ссылка может находиться не в материале, а в пользовательском HTML-модуле. Для таблицы модулей полезно одновременно вывести URL и состояние публикации.

SQL — проверка модулей
SELECT
    id,
    title,
    position,
    published,
    REGEXP_SUBSTR(
        content,
        'https?://[^"''<>[:space:]]+'
    ) AS external_url
FROM PREFIX_modules
WHERE content REGEXP 'https?://'
  AND content NOT REGEXP 'https?://([a-z0-9-]+\\.)*example\\.com'
ORDER BY id;

Проверка внешних URL в модулях Joomla через phpMyAdmin

Обратите внимание на поле published: значение 0 означает, что модуль отключён. Наличие URL в таком модуле ещё не означает, что ссылка реально появляется на страницах сайта. Старые рекламные, партнёрские и подписные модули могут годами оставаться в базе в выключенном состоянии.

Как увидеть только те ссылки, которые реально выводятся в браузере

База данных показывает, где URL хранится. Но чтобы понять, присутствует ли ссылка на конкретной открытой странице, удобнее проверить DOM через консоль браузера.

Откройте нужную страницу сайта и запустите инструменты разработчика одним из удобных способов:

  • F12 — открыть DevTools, затем перейти на вкладку Console;
  • Ctrl + Shift + J — сразу открыть Console в Chrome, Edge или Opera;
  • Ctrl + Shift + I — открыть DevTools, затем выбрать вкладку Console;
  • нажать правой кнопкой мыши по странице и выбрать «Просмотреть код» / «Исследовать элемент» (название пункта зависит от браузера), после чего перейти на вкладку Console.

После открытия Console вставьте следующий код и нажмите Enter:

JavaScript — Console браузера
console.table(
  [...document.querySelectorAll('a[href]')]
    .map(a => {
      const u = new URL(a.href, location.href);
      return {
        text: a.textContent.trim().replace(/\s+/g, ' ').slice(0, 80),
        url: u.href,
        domain: u.hostname,
        rel: a.rel || ''
      };
    })
    .filter(x =>
      /^https?:/.test(x.url) &&
      x.domain !== location.hostname
    )
);

В таблице будут видны текст ссылки, полный URL, домен и значение rel. Если результат пустой, значит на открытой странице нет внешних кликабельных ссылок <a href>.

Поиск внешних ссылок на странице Joomla через Console браузера

Такой способ особенно полезен для проверки конкретной проблемной страницы: URL может храниться в базе или кеше, но не выводиться посетителю.

Поиск конкретного домена в файлах Joomla через ISPmanager

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

В ISPmanager откройте Менеджер файлов → Поиск файлов, укажите корень сайта или конкретную папку, включите:

  • Искать в подкаталогах;
  • Искать по содержимому;
  • в поле «Маска содержимого» — нужный домен, например example.com.

Поиск внешнего домена по содержимому файлов Joomla в ISPmanager

Если поиск по всему сайту находит домен только в каталогах cache или administrator/cache, это ещё не доказательство внедрения в PHP-код. Кеш может содержать сохранённую копию материала или страницы, где такая ссылка была обычной частью текста.

Внешняя ссылка найдена только в кеше Joomla

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

Почему нельзя удалять всё, где встречается base64_decode

base64_decode() — обычная функция PHP для декодирования Base64. Она может использоваться и нормальным кодом Joomla или расширений. Поэтому сам факт её присутствия не доказывает заражение.

Опасный подход — найти все вхождения base64_decode и удалить их. Так можно повредить рабочий компонент, авторизацию, передачу параметров или другую штатную функцию.

Правильнее действовать наоборот:

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

Как безопасно удалить нежелательную ссылку

Способ зависит от источника:

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

Как относиться к обычным внешним ссылкам

Удалять все внешние ссылки ради SEO не нужно. Google рекомендует ссылаться на полезные внешние источники, когда это помогает читателю. Обычная редакционная ссылка не требует nofollow. Для рекламы и платных размещений предпочтителен rel="sponsored", для пользовательских ссылок — ugc, а nofollow применяют, когда вы не хотите связывать свой сайт с ресурсом.

Подробнее: рекомендации Google по исходящим ссылкам.

Итоги

Для проверки внешних и скрытых ссылок в Joomla 5 и 6 необязательно устанавливать отдельные программы. Практичная последовательность выглядит так:

  1. найти внешние URL и домены в материалах через phpMyAdmin;
  2. проверить HTML-модули и их состояние публикации;
  3. на конкретной странице посмотреть реально выведенные ссылки через Console;
  4. если домен подозрительный — найти его по содержимому файлов в ISPmanager;
  5. отделить кеш от реального исходного кода;
  6. удалять только подтверждённую нежелательную вставку и затем повторить проверку.

Такой подход помогает не только найти действительно лишнюю ссылку, но и не повредить сайт случайным удалением нормального кода.

Комментарии  
Добрый день!
Переменные ($returnUri, $serialized_data, $post['back'…) выглядят подозрительно. Я бы проверил их все редактором Notepad++. Для этого выберите файл, в котором они находятся, и поочередно введите название каждой в строке поиска.

Закодированные ссылки в base64 имеют вид в коде "крякозябр", т. е. что-то наподобие вот этого:
PGRpdiBpZD0idnQtbW4iPjxhIGhyZWY9Imh0dHA6Ly
9qb29tbGEtbWFzdGVyLm9yZy8iIHRhcmdldD0iX2JsYW5rIiB0aXR
sZT0i0LvRg9GH0YjQuNC1INGI0LDQsdC70L7QvdGLING
B0LDQudGC0L7QsiDQtNC70Y8gSm9vbWxhIj7Qu9GD0YfRiNC40LUg0Y
jQsNCx0LvQvtC90Ysg0YHQsNC50YLQvtCyINC00Lv
RjyBKb29tbGE8L2E+PGJyPjxhIGhyZWY9Imh0dHA6Ly9iYXR0bGVmaW
VsZDQuY29tLnVhLyIgdGFyZ2V0PSJfYmxhbmsiIHRp
dGxlPSLQvtCx0LfQvtGAINC40LPRgNGLIEJhdHRsZWZpZWxkIDQiPtC+0LHQt
9C+0YAg0LjQs9GA0YsgQmF0dGxlZmllbGQgNDwvYT48L2Rpdj4=

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

Если обнаружите, что это вредоносный код, удалите его полностью, начиная с самого начала.
Добрый вечер!Подскажите пож-та.
У меня после поиска вышло очень много всего,неужели надо чисть все?)
$redirect = base64_decode($returnUri);
if(function_exists('base64_encode') && function_exists('base64_decode'))
if(function_exists('base64_encode') && function_exists('base64_decode'))
$serialized_data = base64_decode($serialized_data);
$ciphertext = base64_decode($ciphertext);
$this->setRedirect(base64_decode($post['back']));
$this->setRedirect(base64_decode($post['back']));
$return = base64_decode($return);
$type = json_decode(base64_decode($type));
$explodeArray = explode(':', base64_decode($fileName));
$override = base64_decode($app->input->get('folder'));
if (base64_decode(urldecode($file)) == 'index.php')
$location = base64_decode($app->input->get('address'));
$location = base64_decode($app->input->get('address'));
Здравствуйте, Андрей! Спасибо за комментарий. Надеюсь, статья оказалась для вас не только интересной, но и полезной :-)
Спасибо большое! Интересная статья
Спасибо за информацию!
Добавить комментарий

Нашли ошибку или неточность?

Выделите фрагмент текста и нажмите Ctrl + Enter. Также можно воспользоваться кнопкой ниже.

Нашли ошибку или неточность?

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

Форма защищена от автоматических отправок

Данные используются только для проверки и исправления материала.